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
@@ -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;
}