Package structure: moved custom recording layout.

Part of #963.
This commit is contained in:
Dennis Guse
2022-01-15 21:28:50 +01:00
parent d49386259a
commit cf993b73d7
23 changed files with 36 additions and 40 deletions
@@ -0,0 +1,75 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
import androidx.annotation.Nullable;
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,16 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
public enum CustomLayoutFieldType {
GENERIC(1),
CLOCK(2);
private final int value;
CustomLayoutFieldType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
@@ -0,0 +1,148 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
import android.content.Context;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.R;
public class DataField implements Parcelable {
public static final String YES_VALUE = "1";
public static final String NOT_VALUE = "0";
private final String key;
private final String title;
private boolean isVisible;
private boolean isPrimary;
private final boolean isWide;
public DataField(String key, String title, boolean isVisible, boolean isPrimary, boolean isWide) {
this.key = key;
this.title = title;
this.isVisible = isVisible;
this.isPrimary = isPrimary;
this.isWide = isWide;
}
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<>() {
@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;
}
public String getTitle() {
return title;
}
public boolean isVisible() {
return isVisible;
}
public void toggleVisibility() {
isVisible = !isVisible;
}
public boolean isPrimary() {
return isPrimary;
}
public void togglePrimary() {
isPrimary = !isPrimary;
}
public boolean isWide() {
return isWide;
}
public CustomLayoutFieldType getType(Context context) {
if (key.equals(context.getString(R.string.stats_custom_layout_clock_key))) {
return CustomLayoutFieldType.CLOCK;
} else {
return CustomLayoutFieldType.GENERIC;
}
}
public String toCsv() {
String visible = this.isVisible ? YES_VALUE : NOT_VALUE;
String primary = this.isPrimary ? YES_VALUE : NOT_VALUE;
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) {
if (key.equals(resources.getString(R.string.stats_custom_layout_total_time_key))) {
return resources.getString(R.string.stats_total_time);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_moving_time_key))) {
return resources.getString(R.string.stats_moving_time);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_distance_key))) {
return resources.getString(R.string.stats_distance);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_speed_key))) {
return resources.getString(R.string.stats_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_pace_key))) {
return resources.getString(R.string.stats_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
return resources.getString(R.string.stats_average_moving_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_speed_key))) {
return resources.getString(R.string.stats_average_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_max_speed_key))) {
return resources.getString(R.string.stats_max_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
return resources.getString(R.string.stats_average_moving_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_pace_key))) {
return resources.getString(R.string.stats_average_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_fastest_pace_key))) {
return resources.getString(R.string.stats_fastest_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_altitude_key))) {
return resources.getString(R.string.stats_altitude);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_gain_key))) {
return resources.getString(R.string.stats_gain);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_loss_key))) {
return resources.getString(R.string.stats_loss);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_coordinates_key))) {
return resources.getString(R.string.stats_coordinates);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_heart_rate_key))) {
return resources.getString(R.string.stats_sensors_heart_rate);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_cadence_key))) {
return resources.getString(R.string.stats_sensors_cadence);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_power_key))) {
return resources.getString(R.string.stats_sensors_power);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_clock_key))) {
return resources.getString(R.string.stats_clock);
} else {
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));
}
}
@@ -0,0 +1,144 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
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;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
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 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<>() {
@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));
}
public void addField(DataField dataField) {
dataFields.add(dataField);
}
public void addFields(List<DataField> dataFields) {
this.dataFields.addAll(dataFields);
}
public void removeField(DataField dataField) {
dataFields.remove(dataField);
}
public void replaceAllFields(List<DataField> newFields) {
dataFields.clear();
addFields(newFields);
}
public List<DataField> getFields() {
return new ArrayList<>(dataFields);
}
public void moveField(int from, int to) {
DataField dataFieldToMove = dataFields.remove(from);
dataFields.add(to, dataFieldToMove);
}
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);
}
}
@@ -0,0 +1,111 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Optional;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Layout layout;
private final Context context;
private final SettingsCustomLayoutItemClickListener itemClickListener;
public SettingsCustomLayoutEditAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
this.context = context;
this.itemClickListener = itemClickListener;
this.layout = layout;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
return new SettingsCustomLayoutEditAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutEditAdapter.ViewHolder viewHolder = (SettingsCustomLayoutEditAdapter.ViewHolder) holder;
DataField field = layout.getFields().get(position);
viewHolder.itemView.setTag(field.getKey());
viewHolder.title.setText(field.getTitle());
viewHolder.value.setText(StatisticsUtils.emptyValue(context, field.getKey()));
viewHolder.title.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel) : R.style.StatsLabelHidden);
viewHolder.value.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue) : R.style.StatsValueHidden);
viewHolder.statusIcon.setVisibility(field.isVisible() ? View.GONE : View.VISIBLE);
viewHolder.statusIcon.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_baseline_visibility_off_24));
viewHolder.moveIcon.setVisibility(field.isVisible() ? View.VISIBLE : View.GONE);
}
@Override
public int getItemCount() {
if (layout == null) {
return 0;
} else {
return layout.getFields().size();
}
}
public boolean isItemWide(int position) {
return layout.getFields().get(position).isWide();
}
public DataField getItem(int position) {
return layout.getFields().get(position);
}
public void swapValues(Layout data) {
this.layout = data;
if (this.layout != null) {
this.notifyDataSetChanged();
}
}
public Layout move(int fromPosition, int toPosition) {
layout.moveField(fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return layout;
}
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView title;
final TextView value;
final TextView unit;
final ImageView statusIcon;
final ImageView moveIcon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.stats_description_main);
value = itemView.findViewById(R.id.stats_value);
unit = itemView.findViewById(R.id.stats_unit);
statusIcon = itemView.findViewById(R.id.stats_icon_show_status);
moveIcon = itemView.findViewById(R.id.stats_iv_drag_indicator);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
String statTitle = (String) view.getTag();
Optional<DataField> optionalField = layout.getFields().stream().filter(f -> f.getKey().equals(statTitle)).findFirst();
optionalField.ifPresent(itemClickListener::onSettingsCustomLayoutItemClicked);
}
}
public interface SettingsCustomLayoutItemClickListener {
void onSettingsCustomLayoutItemClicked(@NonNull DataField field);
}
}
@@ -0,0 +1,94 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
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.settings.PreferencesUtils;
public class SettingsCustomLayoutListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Layout> layoutList;
private final Context context;
private final 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);
}
}