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.
This commit is contained in:
Román Martínez
2021-10-23 10:04:02 +02:00
parent 02cec48ae4
commit 7f9057c519
28 changed files with 1408 additions and 119 deletions
@@ -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<DataField> 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<DataField> 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<DataField> 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<DataField> 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<DataField> 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;");
}
}
@@ -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<Layout> 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<Layout> 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<Layout> 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<Layout> customLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(customLayout.size(), 1);
assertEquals(defaultLayout.toCsv(), customLayout.get(0).toCsv());
}
}
@@ -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<String> partsOk = CsvLayoutUtils.getCsvLineParts(csvLineOk);
List<String> partsWrong1 = CsvLayoutUtils.getCsvLineParts(csvLineWrong1);
List<String> 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);
}
}
@@ -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<Layout> 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<Layout> 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<Layout> 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<Layout> 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<Layout> 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<Layout> 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");
}
}
+2 -1
View File
@@ -324,7 +324,8 @@ limitations under the License.
<activity android:name=".io.file.exporter.ExportActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.SettingsCustomLayoutActivity" />
<activity android:name=".settings.SettingsCustomLayoutListActivity" />
<activity android:name=".settings.SettingsCustomLayoutEditActivity" />
<activity
android:name=".ShowOnMapProxyActivity$KML"
@@ -1,5 +1,6 @@
package de.dennisguse.opentracks;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -18,6 +19,8 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.List;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -263,6 +266,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_insert_marker) {
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
@@ -271,8 +279,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
return true;
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
if (item.getItemId() == R.id.track_detail_menu_select_layout) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
List<String> 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;
}
@@ -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<RecyclerView.ViewHolder> {
public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
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<RecyclerVi
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
return new SettingsCustomLayoutAdapter.ViewHolder(view);
return new SettingsCustomLayoutEditAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutAdapter.ViewHolder viewHolder = (SettingsCustomLayoutAdapter.ViewHolder) holder;
SettingsCustomLayoutEditAdapter.ViewHolder viewHolder = (SettingsCustomLayoutEditAdapter.ViewHolder) holder;
DataField field = layout.getFields().get(position);
viewHolder.itemView.setTag(field.getKey());
viewHolder.title.setText(field.getTitle());
@@ -0,0 +1,95 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.Optional;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class SettingsCustomLayoutListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Layout> 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<Layout> 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<Layout> optionalLayout = layoutList.stream().filter(layout -> layout.sameName(new Layout(profile))).findFirst();
optionalLayout.ifPresent(itemClickListener::onSettingsCustomLayoutProfileClicked);
}
}
public interface SettingsCustomLayoutProfileClickListener {
void onSettingsCustomLayoutProfileClicked(@NonNull Layout layout);
}
}
@@ -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<DataField> CREATOR = new Creator<DataField>() {
@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));
}
}
@@ -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<DataField> dataFields = new ArrayList<>();
public Layout(String profile) {
this.profile = profile;
public static Layout fromCsv(@NonNull String csvLine, @NonNull Resources resources) {
List<String> 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<Layout> CREATOR = new Creator<Layout>() {
@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<DataField> newFields) {
dataFields.clear();
addFields(newFields);
}
public List<DataField> 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<DataField> 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);
}
}
@@ -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;
}
@@ -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<String> 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());
}
}
@@ -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<TypedArray> getMultiTypedArray(String key) {
@@ -576,46 +574,84 @@ public class PreferencesUtils {
}
@SuppressLint("ResourceType")
static String buildDefaultLayout() {
private static String buildDefaultFields() {
List<TypedArray> 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<String> csvParts = Arrays.asList(csvCustomLayout.split(CsvConstants.LINE_SEPARATOR));
Layout layout = new Layout(csvParts.get(0));
for (int i = 1; i < csvParts.size(); i++) {
String[] fieldParts = csvParts.get(i).split(CsvConstants.ITEM_SEPARATOR);
layout.addField(fieldParts[0], DataField.getTitleByKey(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<DataField> fields = layout.getFields();
if (fields.isEmpty()) {
return;
public static void updateCustomLayouts(@NonNull List<Layout> 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<Layout> preferenceLayouts = PreferencesUtils.getAllCustomLayouts();
Optional<Layout> 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<Layout> getAllCustomLayouts() {
List<Layout> 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<String> 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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
});
@@ -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 = ",";
}
@@ -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<String> 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<String> 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);
}
}
@@ -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;
}
}
@@ -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;
}
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<LinearLayout
android:id="@+id/custom_layout_list_add_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_marginBottom="50dp"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/custom_layout_list_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
android:layout_margin="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_layout_list_edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:imeOptions="actionDone"
android:hint="@string/custom_layout_list_edit_name" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/custom_layout_list_bottom_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/custom_layout_list_cancel_button"
style="@style/BottomButton"
android:text="@android:string/cancel" />
<Button
android:id="@+id/custom_layout_list_ok_button"
style="@style/BottomButton"
android:text="@android:string/ok" />
</LinearLayout>
<View style="@style/HorizontalLine" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/StatsCardView">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/custom_layout_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Profile/Custom Layout Title"
style="@style/LargeLabel"
android:layout_margin="8dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/custom_layout_edit_add_profile"
android:icon="@drawable/ic_baseline_add_24"
android:orderInCategory="1"
app:showAsAction="ifRoom"
android:title="@string/menu_add_custom_layout" />
</menu>
+9 -3
View File
@@ -15,6 +15,11 @@ limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_map_24dp"
app:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" />
<item
android:id="@+id/track_detail_insert_marker"
android:icon="@drawable/ic_marker_add_24dp"
@@ -22,10 +27,11 @@ limitations under the License.
app:showAsAction="ifRoom"
android:title="@string/menu_insert_marker" />
<item
android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_map_24dp"
android:id="@+id/track_detail_menu_select_layout"
android:icon="@drawable/ic_ui"
android:orderInCategory="2"
app:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" />
android:title="@string/menu_select_layout" />
<item
android:id="@+id/track_detail_markers"
android:icon="@drawable/ic_marker_show_24dp"
+3
View File
@@ -30,6 +30,9 @@ limitations under the License.
<color name="colorAccent">@android:color/holo_orange_dark</color>
<color name="colorAccentTransparent">#55FF8800</color>
<color name="colorBackgroundDelete">#F44336</color>
<color name="colorForegroundDelete">#FEFEFE</color>
<color name="recording_text">#FFBE161B</color> <!-- red -->
<color name="stats_separator">#99999999</color> <!-- light gray -->
<color name="text_gradient_center">#B2000000</color> <!-- black 70% opacity -->
+3 -2
View File
@@ -46,6 +46,7 @@
<string name="settings_import" translatable="false">settingsImport</string>
<string name="settings_export" translatable="false">settingsExport</string>
<!-- Stats layout customization default layout -->
<string name="default_activity_key" translatable="false">defaultActivity</string>
<string name="default_activity_default" translatable="false">@string/activity_type_unknown</string>
<string-array name="activity_types">
@@ -310,11 +311,11 @@
<string name="stats_custom_layout_key" translatable="false">statsCustomLayoutKey</string>
<!-- Stats layout customization: number of columns per row -->
<string name="stats_custom_layout_columns_key" translatable="false">statsCustomLayoutColumnsKey</string>
<integer name="stats_custom_layout_columns_default" translatable="false">2</integer>
<!-- Stats layout customization: key -->
<string name="stats_custom_layout_fields_key" translatable="false">statsCustomLayoutFieldsKey</string>
<string name="stats_custom_layouts_key" translatable="false">statsCustomLayoutFieldsKey</string>
<string name="stats_custom_layout_selected_layout_key" translatable="false">statsCustomSelectedLayoutKey</string>
<!-- Stats keys -->
<string name="stats_custom_layout_distance_key" translatable="false">distance</string>
<string name="stats_custom_layout_total_time_key" translatable="false">total_time</string>
+10
View File
@@ -238,6 +238,7 @@ limitations under the License.
<string name="generic_completed">Completed</string>
<string name="generic_click_twice_cancel">Click back again to cancel</string>
<string name="generic_choose_an_option">Choose an option</string>
<string name="generic_undo">Undo</string>
<!-- Gps -->
<string name="gps_starting">Starting GPS</string>
<string name="gps_wait_for_better_signal">Waiting for a better GPS signal</string>
@@ -298,11 +299,13 @@ limitations under the License.
<string name="menu_select_all">Select all</string>
<string name="menu_settings">Settings</string>
<string name="menu_share">Share</string>
<string name="menu_select_layout">Select Layout</string>
<string name="menu_show_on_map">Show on map</string>
<string name="menu_start_gps">Start GPS</string>
<string name="menu_stop_gps">Stop GPS</string>
<string name="menu_voice_rate">Voice speed</string>
<string name="menu_resume_track">Resume track</string>
<string name="menu_add_custom_layout">New Layout</string>
<!-- OpenTracks -->
<string name="recording_service">Recording service</string>
<string name="delete_service">Deletion service</string>
@@ -395,6 +398,13 @@ limitations under the License.
<string name="settings_announcements_distance_frequency">Distance interval</string>
<string name="settings_reset">Reset</string>
<!-- Custom Layout -->
<string name="custom_layout_list_title">Layouts</string>
<string name="custom_layout_list_edit_name">Type layout\'s name to add</string>
<string name="custom_layout_list_edit_already_exists">Layout\'s name already exists</string>
<string name="custom_layout_list_layout_removed">Layout was removed</string>
<string name="custom_layout_select_layout">Select Layout</string>
<string name="stats_custom_layout_default_layout">Default Layout</string>
<!-- Custom Layout Reset -->
<string name="settings_layout_reset">Reset Custom Layout</string>
<string name="settings_layout_reset_confirm_message">All layout settings will be reverted to the default values. This will not delete any tracks on the device.</string>