1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-27 12:26:48 +01:00

Refactor activity lists to use a RecyclerView

This commit is contained in:
José Rebelo 2024-10-06 20:16:09 +01:00
parent 32e955abe2
commit 22a6d9b7d9
21 changed files with 981 additions and 880 deletions

View File

@ -17,7 +17,9 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.widget.ListView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
@ -26,7 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.adapter.AbstractActivityListingAdapt
public abstract class AbstractListActivity<T> extends AbstractGBActivity {
private AbstractActivityListingAdapter<T> itemAdapter;
private ListView itemListView;
private RecyclerView itemListView;
public void setItemAdapter(AbstractActivityListingAdapter<T> itemAdapter) {
this.itemAdapter = itemAdapter;
@ -37,23 +39,23 @@ public abstract class AbstractListActivity<T> extends AbstractGBActivity {
this.itemAdapter.loadItems();
}
public void setActivityKindFilter(int activityKind){
public void setActivityKindFilter(int activityKind) {
this.itemAdapter.setActivityKindFilter(activityKind);
}
public void setDateFromFilter(long date){
public void setDateFromFilter(long date) {
this.itemAdapter.setDateFromFilter(date);
}
public void setDateToFilter(long date){
public void setDateToFilter(long date) {
this.itemAdapter.setDateToFilter(date);
}
public void setNameContainsFilter(String name){
public void setNameContainsFilter(String name) {
this.itemAdapter.setNameContainsFilter(name);
}
public void setItemsFilter(List items) {
public void setItemsFilter(List<Long> items) {
this.itemAdapter.setItemsFilter(items);
}
@ -65,15 +67,12 @@ public abstract class AbstractListActivity<T> extends AbstractGBActivity {
return itemAdapter;
}
public ListView getItemListView() {
return itemListView;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
itemListView = findViewById(R.id.itemListView);
itemListView.setLayoutManager(new LinearLayoutManager(this));
}
}

View File

@ -34,13 +34,13 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.DatePicker;
import android.widget.ListView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -49,6 +49,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedHashMap;
@ -66,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -80,7 +82,10 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
List<Long> itemsFilter;
String nameContainsFilter;
private GBDevice mGBDevice;
private BitSet selectedItems;
private SwipeRefreshLayout swipeLayout;
private ActionMode mActionMode;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -160,7 +165,6 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
if (requestCode == ACTIVITY_DETAIL) {
refresh();
}
}
@Override
@ -177,62 +181,96 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
ActivitySummariesAdapter activitySummariesAdapter = new ActivitySummariesAdapter(this, mGBDevice, activityFilter, dateFromFilter, dateToFilter, nameContainsFilter, deviceFilter, itemsFilter);
int backgroundColor = getBackgroundColor(ActivitySummariesActivity.this);
activitySummariesAdapter.setBackgroundColor(backgroundColor);
activitySummariesAdapter.setShowTime(false);
setItemAdapter(activitySummariesAdapter);
getItemListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == 0) return; // item 0 is empty for dashboard
Object item = parent.getItemAtPosition(position);
if (item != null) {
ActivitySummary summary = (ActivitySummary) item;
try {
showActivityDetail(position);
} catch (Exception e) {
GB.toast(getApplicationContext(), "Unable to display Activity Detail, maybe the activity is not available yet: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
selectedItems = activitySummariesAdapter.getSelectedItems();
activitySummariesAdapter.setOnItemClickListener(position -> {
if (!selectedItems.isEmpty()) {
selectedItems.set(position, !selectedItems.get(position));
activitySummariesAdapter.notifyItemChanged(position);
if (!selectedItems.isEmpty()) {
startActionMode();
} else {
stopActionMode();
}
return;
}
if (position == 0) return; // item 0 is empty for dashboard
ActivitySummary summary = activitySummariesAdapter.getItem(position);
if (summary != null) {
try {
showActivityDetail(position);
} catch (Exception e) {
GB.toast(getApplicationContext(), "Unable to display Activity Detail, maybe the activity is not available yet: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
});
activitySummariesAdapter.setOnItemLongClickListener(position -> {
selectedItems.set(position, !selectedItems.get(position));
activitySummariesAdapter.notifyItemChanged(position);
getItemListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
getItemListView().setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public void onItemCheckedStateChanged(ActionMode actionMode, int position, long id, boolean checked) {
if (position == 0 && checked) subtrackDashboard = 1;
if (position == 0 && !checked) subtrackDashboard = 0;
final int selectedItems = getItemListView().getCheckedItemCount() - subtrackDashboard;
actionMode.setTitle(selectedItems + " selected");
if (!selectedItems.isEmpty()) {
startActionMode();
} else {
stopActionMode();
}
});
setItemAdapter(activitySummariesAdapter);
swipeLayout = findViewById(R.id.list_activity_swipe_layout);
swipeLayout.setOnRefreshListener(this::fetchTrackData);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> fetchTrackData());
activityKindMap = fillKindMap();
}
private void stopActionMode() {
if (mActionMode != null) {
mActionMode.finish();
mActionMode = null;
}
}
private void startActionMode() {
int[] numSelected = new int[]{0};
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
numSelected[0]++;
}
}
if (mActionMode != null) {
// already in action mode
mActionMode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
return;
}
mActionMode = startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
mode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
getMenuInflater().inflate(R.menu.activity_list_context_menu, menu);
findViewById(R.id.fab).setVisibility(View.INVISIBLE);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
boolean processed = false;
final SparseBooleanArray checked = getItemListView().getCheckedItemPositions();
final int itemId = menuItem.getItemId();
if (itemId == R.id.activity_action_delete) {
final List<BaseActivitySummary> toDelete = new ArrayList<>();
for (int i = 0; i < checked.size(); i++) {
if (checked.valueAt(i)) {
toDelete.add(getItemAdapter().getItem(checked.keyAt(i)));
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
toDelete.add(getItemAdapter().getItem(i));
}
}
@ -252,16 +290,13 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
} else if (itemId == R.id.activity_action_export) {
final List<String> paths = new ArrayList<>();
for (int i = 0; i < checked.size(); i++) {
if (checked.valueAt(i)) {
BaseActivitySummary item = getItemAdapter().getItem(checked.keyAt(i));
if (item != null) {
ActivitySummary summary = item;
String gpxTrack = summary.getGpxTrack();
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
BaseActivitySummary summary = getItemAdapter().getItem(i);
if (summary != null) {
File gpxTrack = ActivitySummaryUtils.getGpxFile(summary);
if (gpxTrack != null) {
paths.add(gpxTrack);
paths.add(gpxTrack.getPath());
}
}
}
@ -269,17 +304,21 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
shareMultiple(paths);
processed = true;
} else if (itemId == R.id.activity_action_select_all) {
for (int i = 0; i < getItemListView().getCount(); i++) {
getItemListView().setItemChecked(i, true);
for (int i = 1; i < getItemAdapter().getItemCount() - 1; i++) {
if (!selectedItems.get(i)) {
numSelected[0]++;
selectedItems.set(i, true);
getItemAdapter().notifyItemChanged(i);
}
}
return true; //don't finish actionmode in this case!
actionMode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
return true; //don't finish actionMode in this case!
} else if (itemId == R.id.activity_action_addto_filter) {
final List<Long> toFilter = new ArrayList<>();
for (int i = 0; i < checked.size(); i++) {
if (checked.valueAt(i)) {
BaseActivitySummary item = getItemAdapter().getItem(checked.keyAt(i));
if (item != null && item.getId() != null) {
ActivitySummary summary = item;
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
BaseActivitySummary summary = getItemAdapter().getItem(i);
if (summary != null && summary.getId() != null) {
Long id = summary.getId();
toFilter.add(id);
}
@ -292,34 +331,22 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
processed = true;
}
actionMode.finish();
mActionMode = null;
return processed;
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
public void onDestroyActionMode(final ActionMode mode) {
mActionMode = null;
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
selectedItems.set(i, false);
getItemAdapter().notifyItemChanged(i);
}
}
findViewById(R.id.fab).setVisibility(View.VISIBLE);
}
});
swipeLayout = findViewById(R.id.list_activity_swipe_layout);
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
fetchTrackData();
}
});
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fetchTrackData();
}
});
activityKindMap = fillKindMap();
}
private LinkedHashMap<String, ActivityKind> fillKindMap() {
@ -362,11 +389,11 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
for (BaseActivitySummary item : items) {
try {
item.delete();
getItemAdapter().remove(item);
} catch (Exception e) {
//pass delete error
}
}
// Adapter is fully reloaded after refresh
refresh();
}
@ -398,7 +425,6 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
} else {
GB.toast(this, "No selected activity contains a GPX track to share", Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void showActivityDetail(int position) {

View File

@ -75,7 +75,6 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -85,18 +84,13 @@ import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.Activity
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -578,71 +572,33 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
private void viewGpxTrack(Context context) {
final File trackFile = getTrackFile();
if (trackFile == null) {
final File gpxFile = ActivitySummaryUtils.getGpxFile(currentItem);
if (gpxFile == null) {
GB.toast(getApplicationContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO);
return;
}
try {
if (trackFile.getName().endsWith(".gpx")) {
AndroidUtils.viewFile(trackFile.getPath(), "application/gpx+xml", context);
} else if (trackFile.getName().endsWith(".fit")) {
final File gpxFile = convertFitToGpx(trackFile);
AndroidUtils.viewFile(gpxFile.getPath(), "application/gpx+xml", context);
} else {
GB.toast(getApplicationContext(), "Unknown track format", Toast.LENGTH_LONG, GB.INFO);
}
AndroidUtils.viewFile(gpxFile.getPath(), "application/gpx+xml", context);
} catch (final Exception e) {
GB.toast(getApplicationContext(), "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
private void shareGpxTrack(final Context context) {
final File trackFile = getTrackFile();
if (trackFile == null) {
final File gpxFile = ActivitySummaryUtils.getGpxFile(currentItem);
if (gpxFile == null) {
GB.toast(getApplicationContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO);
return;
}
try {
if (trackFile.getName().endsWith(".gpx")) {
AndroidUtils.shareFile(context, trackFile);
} else if (trackFile.getName().endsWith(".fit")) {
final File gpxFile = convertFitToGpx(trackFile);
AndroidUtils.shareFile(context, gpxFile);
} else {
GB.toast(getApplicationContext(), "Unknown track format", Toast.LENGTH_LONG, GB.INFO);
}
AndroidUtils.shareFile(context, gpxFile);
} catch (final Exception e) {
GB.toast(context, "Unable to share GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
private File convertFitToGpx(final File file) throws IOException, ActivityTrackExporter.GPXTrackEmptyException {
final FitFile fitFile = FitFile.parseIncoming(file);
final List<ActivityPoint> activityPoints = fitFile.getRecords().stream()
.filter(r -> r instanceof FitRecord)
.map(r -> ((FitRecord) r).toActivityPoint())
.filter(ap -> ap.getLocation() != null)
.collect(Collectors.toList());
final ActivityTrack activityTrack = new ActivityTrack();
activityTrack.setName(currentItem.getName());
activityTrack.addTrackPoints(activityPoints);
final File cacheDir = getCacheDir();
final File rawCacheDir = new File(cacheDir, "gpx");
//noinspection ResultOfMethodCallIgnored
rawCacheDir.mkdir();
final File gpxFile = new File(rawCacheDir, file.getName().replace(".fit", ".gpx"));
final GPXExporter gpxExporter = new GPXExporter();
gpxExporter.performExport(activityTrack, gpxFile);
return gpxFile;
}
private static void shareRawSummary(final Context context, final BaseActivitySummary summary) {
if (summary.getRawSummaryData() == null) {
GB.toast(context, "No raw summary in this activity", Toast.LENGTH_LONG, GB.WARN);
@ -732,15 +688,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
@Nullable
private File getTrackFile() {
final String gpxTrack = currentItem.getGpxTrack();
if (gpxTrack != null) {
return FileUtils.tryFixPath(new File(gpxTrack));
}
final String rawDetails = currentItem.getRawDetailsPath();
if (rawDetails != null && rawDetails.endsWith(".fit")) {
return FileUtils.tryFixPath(new File(rawDetails));
}
return null;
return ActivitySummaryUtils.getTrackFile(currentItem);
}
@Override

View File

@ -24,6 +24,9 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.data.PieData;
@ -36,14 +39,11 @@ import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.AbstractActivityListingAdapter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityListItem;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
@ -54,8 +54,6 @@ public class ActivityListingAdapter extends AbstractActivityListingAdapter<Activ
public static final String CHART_COLOR_END = "#2ecc71";
protected static final Logger LOG = LoggerFactory.getLogger(ActivityListingAdapter.class);
protected final int ANIM_TIME = 250;
private final int SESSION_SUMMARY = ActivitySession.SESSION_SUMMARY;
private final int SESSION_EMPTY = ActivitySession.SESSION_EMPTY;
ActivityUser activityUser = new ActivityUser();
int stepsGoal = activityUser.getStepsGoal();
int distanceGoalMeters = activityUser.getDistanceGoalMeters();
@ -65,109 +63,27 @@ public class ActivityListingAdapter extends AbstractActivityListingAdapter<Activ
super(context);
}
@NonNull
@Override
protected View fill_dashboard(ActivitySession item, int position, View view, ViewGroup parent, Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.activity_list_dashboard_item, parent, false);
PieChart ActiveStepsChart;
PieChart DistanceChart;
PieChart ActiveTimeChart;
ActiveStepsChart = view.findViewById(R.id.activity_dashboard_piechart1);
setUpChart(ActiveStepsChart);
int steps = item.getActiveSteps();
setChartsData(ActiveStepsChart, steps, stepsGoal, context.getString(R.string.activity_list_summary_active_steps), context);
DistanceChart = view.findViewById(R.id.activity_dashboard_piechart2);
setUpChart(DistanceChart);
float distance = item.getDistance();
setChartsData(DistanceChart, distance, distanceGoalMeters, context.getString(R.string.distance), context);
ActiveTimeChart = view.findViewById(R.id.activity_dashboard_piechart3);
setUpChart(ActiveTimeChart);
long duration = item.getEndTime().getTime() - item.getStartTime().getTime();
setChartsData(ActiveTimeChart, duration, activeTimeGoalTimeMillis, context.getString(R.string.activity_list_summary_active_time), context);
TextView stepLabel = view.findViewById(R.id.line_layout_step_label);
TextView stepTotalLabel = view.findViewById(R.id.line_layout_total_step_label);
TextView distanceLabel = view.findViewById(R.id.line_layout_distance_label);
TextView hrLabel = view.findViewById(R.id.heartrate_widget_label);
TextView intensityLabel = view.findViewById(R.id.intensity_widget_label);
TextView intensity2Label = view.findViewById(R.id.line_layout_intensity2_label);
TextView durationLabel = view.findViewById(R.id.line_layout_duration_label);
TextView sessionCountLabel = view.findViewById(R.id.line_layout_count_label);
LinearLayout durationLayout = view.findViewById(R.id.line_layout_duration);
LinearLayout countLayout = view.findViewById(R.id.line_layout_count);
View hrLayout = view.findViewById(R.id.heartrate_widget_icon);
LinearLayout stepsLayout = view.findViewById(R.id.line_layout_step);
LinearLayout stepsTotalLayout = view.findViewById(R.id.line_layout_total_step);
LinearLayout distanceLayout = view.findViewById(R.id.line_layout_distance);
View intensityLayout = view.findViewById(R.id.intensity_widget_icon);
View intensity2Layout = view.findViewById(R.id.line_layout_intensity2);
stepLabel.setText(getStepLabel(item));
stepTotalLabel.setText(getStepTotalLabel(item));
distanceLabel.setText(getDistanceLabel(item));
hrLabel.setText(getHrLabel(item));
intensityLabel.setText(getIntensityLabel(item));
intensity2Label.setText(getIntensityLabel(item));
durationLabel.setText(getDurationLabel(item));
sessionCountLabel.setText(getSessionCountLabel(item));
if (!hasHR(item)) {
hrLayout.setVisibility(View.GONE);
hrLabel.setVisibility(View.GONE);
} else {
hrLayout.setVisibility(View.VISIBLE);
hrLabel.setVisibility(View.VISIBLE);
public AbstractActivityListingViewHolder<ActivitySession> onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
switch (viewType) {
case 0: // dashboard
return new DashboardViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_dashboard_item, parent, false));
case 2: // item
return new ActivityItemViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_item, parent, false));
}
if (!hasIntensity(item)) {
intensityLayout.setVisibility(View.GONE);
intensity2Layout.setVisibility(View.GONE);
intensityLabel.setVisibility(View.GONE);
intensity2Label.setVisibility(View.GONE);
} else {
intensityLayout.setVisibility(View.VISIBLE);
intensity2Layout.setVisibility(View.VISIBLE);
intensityLabel.setVisibility(View.VISIBLE);
intensity2Label.setVisibility(View.VISIBLE);
}
if (!hasDistance(item)) {
distanceLayout.setVisibility(View.GONE);
} else {
distanceLayout.setVisibility(View.VISIBLE);
}
if (!hasSteps(item)) {
stepsLayout.setVisibility(View.GONE);
} else {
stepsLayout.setVisibility(View.VISIBLE);
}
if (!hasTotalSteps(item)) {
stepsTotalLayout.setVisibility(View.GONE);
countLayout.setVisibility(View.GONE);
durationLayout.setVisibility(View.GONE);
} else {
stepsTotalLayout.setVisibility(View.VISIBLE);
countLayout.setVisibility(View.VISIBLE);
durationLayout.setVisibility(View.VISIBLE);
}
return view;
return super.onCreateViewHolder(parent, viewType);
}
private void setChartsData(PieChart pieChart, float value, float target, String label, Context context) {
ArrayList<PieEntry> entries = new ArrayList<>();
entries.add(new PieEntry((float) value, context.getResources().getDrawable(R.drawable.ic_star_gold)));
entries.add(new PieEntry(value, AppCompatResources.getDrawable(context, R.drawable.ic_star_gold)));
Easing.EasingFunction animationEffect = Easing.EaseInOutSine;
if (value < target) {
entries.add(new PieEntry((float) (target - value)));
entries.add(new PieEntry(target - value));
}
pieChart.setCenterText(String.format("%d%%\n%s", (int) (value * 100 / target), label));
@ -194,22 +110,6 @@ public class ActivityListingAdapter extends AbstractActivityListingAdapter<Activ
pieChart.animateY(ANIM_TIME, animationEffect);
}
private void setUpChart(PieChart DashboardChart) {
DashboardChart.setNoDataText("");
DashboardChart.getLegend().setEnabled(false);
DashboardChart.setDrawHoleEnabled(true);
DashboardChart.setHoleColor(Color.WHITE);
DashboardChart.getDescription().setText("");
DashboardChart.setTransparentCircleColor(Color.WHITE);
DashboardChart.setTransparentCircleAlpha(110);
DashboardChart.setHoleRadius(70f);
DashboardChart.setTransparentCircleRadius(75f);
DashboardChart.setDrawCenterText(true);
DashboardChart.setRotationEnabled(true);
DashboardChart.setHighlightPerTapEnabled(true);
DashboardChart.setCenterTextOffset(0, 0);
}
private float interpolate(float a, float b, float proportion) {
return (a + ((b - a) * proportion));
}
@ -225,135 +125,162 @@ public class ActivityListingAdapter extends AbstractActivityListingAdapter<Activ
return Color.HSVToColor(hsvb);
}
@Override
protected String getDateLabel(ActivitySession item) {
return "";
}
public static class ActivityItemViewHolder extends AbstractActivityListingViewHolder<ActivitySession> {
final View rootView;
final ActivityListItem activityListItem;
@Override
protected boolean hasGPS(ActivitySession item) {
return false;
}
@Override
protected boolean hasDate(ActivitySession item) {
return false;
}
@Override
protected String getTimeFrom(ActivitySession item) {
Date time = item.getStartTime();
return DateTimeUtils.formatTime(time.getHours(), time.getMinutes());
}
@Override
protected String getTimeTo(ActivitySession item) {
Date time = item.getEndTime();
return DateTimeUtils.formatTime(time.getHours(), time.getMinutes());
}
@Override
protected String getActivityName(ActivitySession item) {
return item.getActivityKind().getLabel(getContext());
}
@Override
protected String getStepLabel(ActivitySession item) {
return String.valueOf(item.getActiveSteps());
}
@Override
protected String getDistanceLabel(ActivitySession item) {
return FormatUtils.getFormattedDistanceLabel(item.getDistance());
}
@Override
protected String getHrLabel(ActivitySession item) {
return String.valueOf(item.getHeartRateAverage());
}
@Override
protected String getIntensityLabel(ActivitySession item) {
DecimalFormat df = new DecimalFormat("###");
return df.format(item.getIntensity());
}
@Override
protected String getDurationLabel(ActivitySession item) {
long duration = item.getEndTime().getTime() - item.getStartTime().getTime();
return DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS);
}
@Override
protected String getSpeedLabel(ActivitySession item) {
long duration = item.getEndTime().getTime() - item.getStartTime().getTime();
double distanceMeters = item.getDistance();
double speed = distanceMeters * 1000 / duration;
String unit = "###.#km/h";
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
unit = "###.#mi/h";
speed = speed * 0.6213712;
public ActivityItemViewHolder(@NonNull final View itemView) {
super(itemView);
this.rootView = itemView;
this.activityListItem = new ActivityListItem(itemView);
}
@Override
public void fill(final int position, final ActivitySession session, final boolean selected) {
this.activityListItem.update(
session.getStartTime(),
session.getEndTime(),
session.getActivityKind(),
null,
session.getActiveSteps(),
session.getDistance(),
session.getHeartRateAverage(),
session.getIntensity(),
session.getEndTime().getTime() - session.getStartTime().getTime(),
false,
null,
position % 2 == 1,
selected
);
}
DecimalFormat df = new DecimalFormat(unit);
return df.format(speed);
}
@Override
protected String getSessionCountLabel(ActivitySession item) {
return String.valueOf(item.getSessionCount());
}
public class DashboardViewHolder extends AbstractActivityListingViewHolder<ActivitySession> {
final PieChart ActiveStepsChart;
final PieChart DistanceChart;
final PieChart ActiveTimeChart;
final TextView stepLabel;
final TextView stepTotalLabel;
final TextView distanceLabel;
final TextView hrLabel;
final TextView intensityLabel;
final TextView intensity2Label;
final TextView durationLabel;
final TextView sessionCountLabel;
final LinearLayout durationLayout;
final LinearLayout countLayout;
final View hrLayout;
final LinearLayout stepsLayout;
final LinearLayout stepsTotalLayout;
final LinearLayout distanceLayout;
final View intensityLayout;
final View intensity2Layout;
@Override
public boolean hasHR(ActivitySession item) {
return item.getHeartRateAverage() > 0;
}
public DashboardViewHolder(@NonNull final View itemView) {
super(itemView);
@Override
public boolean hasIntensity(ActivitySession item) {
return item.getIntensity() > 0;
}
ActiveStepsChart = itemView.findViewById(R.id.activity_dashboard_piechart1);
DistanceChart = itemView.findViewById(R.id.activity_dashboard_piechart2);
ActiveTimeChart = itemView.findViewById(R.id.activity_dashboard_piechart3);
stepLabel = itemView.findViewById(R.id.line_layout_step_label);
stepTotalLabel = itemView.findViewById(R.id.line_layout_total_step_label);
distanceLabel = itemView.findViewById(R.id.line_layout_distance_label);
hrLabel = itemView.findViewById(R.id.heartrate_widget_label);
intensityLabel = itemView.findViewById(R.id.intensity_widget_label);
intensity2Label = itemView.findViewById(R.id.line_layout_intensity2_label);
durationLabel = itemView.findViewById(R.id.line_layout_duration_label);
sessionCountLabel = itemView.findViewById(R.id.line_layout_count_label);
durationLayout = itemView.findViewById(R.id.line_layout_duration);
countLayout = itemView.findViewById(R.id.line_layout_count);
hrLayout = itemView.findViewById(R.id.heartrate_widget_icon);
stepsLayout = itemView.findViewById(R.id.line_layout_step);
stepsTotalLayout = itemView.findViewById(R.id.line_layout_total_step);
distanceLayout = itemView.findViewById(R.id.line_layout_distance);
intensityLayout = itemView.findViewById(R.id.intensity_widget_icon);
intensity2Layout = itemView.findViewById(R.id.line_layout_intensity2);
}
@Override
protected boolean hasDistance(ActivitySession item) {
return item.getDistance() > 0;
}
@Override
public void fill(final int position, final ActivitySession session, final boolean selected) {
setUpChart(ActiveStepsChart);
int steps = session.getActiveSteps();
setChartsData(ActiveStepsChart, steps, stepsGoal, getContext().getString(R.string.activity_list_summary_active_steps), getContext());
@Override
protected boolean hasSteps(ActivitySession item) {
return item.getActiveSteps() > 0;
}
setUpChart(DistanceChart);
float distance = session.getDistance();
setChartsData(DistanceChart, distance, distanceGoalMeters, getContext().getString(R.string.distance), getContext());
@Override
protected boolean hasTotalSteps(ActivitySession item) {
return item.getTotalDaySteps() > 0;
}
setUpChart(ActiveTimeChart);
long duration = session.getEndTime().getTime() - session.getStartTime().getTime();
setChartsData(ActiveTimeChart, duration, activeTimeGoalTimeMillis, getContext().getString(R.string.activity_list_summary_active_time), getContext());
@Override
protected boolean isSummary(ActivitySession item, int position) {
int sessionType = item.getSessionType();
return sessionType == SESSION_SUMMARY;
}
durationLabel.setText(DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS));
sessionCountLabel.setText(String.valueOf(session.getSessionCount()));
@Override
protected boolean isEmptySession(ActivitySession item, int position) {
int sessionType = item.getSessionType();
return sessionType == SESSION_EMPTY;
}
if (session.getHeartRateAverage() > 0) {
hrLabel.setText(String.valueOf(session.getHeartRateAverage()));
hrLayout.setVisibility(View.VISIBLE);
hrLabel.setVisibility(View.VISIBLE);
} else {
hrLayout.setVisibility(View.GONE);
hrLabel.setVisibility(View.GONE);
}
@Override
protected boolean isEmptySummary(ActivitySession item) {
return item.getIsEmptySummary();
}
if (session.getIntensity() > 0) {
final DecimalFormat df = new DecimalFormat("###");
intensityLabel.setText(df.format(session.getIntensity()));
intensity2Label.setText(df.format(session.getIntensity()));
intensityLayout.setVisibility(View.VISIBLE);
intensity2Layout.setVisibility(View.VISIBLE);
intensityLabel.setVisibility(View.VISIBLE);
intensity2Label.setVisibility(View.VISIBLE);
} else {
intensityLayout.setVisibility(View.GONE);
intensity2Layout.setVisibility(View.GONE);
intensityLabel.setVisibility(View.GONE);
intensity2Label.setVisibility(View.GONE);
}
@Override
protected String getStepTotalLabel(ActivitySession item) {
return String.valueOf(item.getTotalDaySteps());
}
if (session.getDistance() > 0) {
distanceLabel.setText(FormatUtils.getFormattedDistanceLabel(session.getDistance()));
distanceLayout.setVisibility(View.VISIBLE);
} else {
distanceLayout.setVisibility(View.GONE);
}
@Override
protected int getIcon(ActivitySession item) {
return item.getActivityKind().getIcon();
if (session.getActiveSteps() > 0) {
stepLabel.setText(String.valueOf(session.getActiveSteps()));
stepsLayout.setVisibility(View.VISIBLE);
} else {
stepsLayout.setVisibility(View.GONE);
}
if (session.getTotalDaySteps() > 0) {
stepTotalLabel.setText(String.valueOf(session.getTotalDaySteps()));
stepsTotalLayout.setVisibility(View.VISIBLE);
countLayout.setVisibility(View.VISIBLE);
durationLayout.setVisibility(View.VISIBLE);
} else {
stepsTotalLayout.setVisibility(View.GONE);
countLayout.setVisibility(View.GONE);
durationLayout.setVisibility(View.GONE);
}
}
private void setUpChart(PieChart DashboardChart) {
DashboardChart.setNoDataText("");
DashboardChart.getLegend().setEnabled(false);
DashboardChart.setDrawHoleEnabled(true);
DashboardChart.setHoleColor(Color.WHITE);
DashboardChart.getDescription().setText("");
DashboardChart.setTransparentCircleColor(Color.WHITE);
DashboardChart.setTransparentCircleAlpha(110);
DashboardChart.setHoleRadius(70f);
DashboardChart.setTransparentCircleRadius(75f);
DashboardChart.setDrawCenterText(true);
DashboardChart.setRotationEnabled(true);
DashboardChart.setHighlightPerTapEnabled(true);
DashboardChart.setCenterTextOffset(0, 0);
}
}
}

View File

@ -24,11 +24,11 @@ import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.github.mikephil.charting.charts.Chart;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -48,6 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
public class ActivityListingChartFragment extends AbstractActivityChartFragment<ActivityListingChartFragment.MyChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(ActivityListingChartFragment.class);
@ -62,19 +64,17 @@ public class ActivityListingChartFragment extends AbstractActivityChartFragment<
Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_steps_list, container, false);
getChartsHost().enableSwipeRefresh(false);
ListView stepsList = rootView.findViewById(R.id.itemListView);
RecyclerView stepsList = rootView.findViewById(R.id.itemListView);
stepListAdapter = new ActivityListingAdapter(getContext());
stepsList.setAdapter(stepListAdapter);
stepsList.setLayoutManager(new LinearLayoutManager(requireContext()));
stepsList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
ActivitySession item = stepListAdapter.getItem(i);
if (item.getSessionType() != ActivitySession.SESSION_SUMMARY) {
int tsFrom = (int) (item.getStartTime().getTime() / 1000);
int tsTo = (int) (item.getEndTime().getTime() / 1000);
showDetail(tsFrom, tsTo, item, getChartsHost().getDevice());
}
stepListAdapter.setOnItemClickListener(position -> {
ActivitySession item = stepListAdapter.getItem(position);
if (item.getSessionType() != ActivitySession.SESSION_SUMMARY) {
int tsFrom = (int) (item.getStartTime().getTime() / 1000);
int tsTo = (int) (item.getEndTime().getTime() / 1000);
showDetail(tsFrom, tsTo, item, getChartsHost().getDevice());
}
});
@ -99,11 +99,15 @@ public class ActivityListingChartFragment extends AbstractActivityChartFragment<
return "Steps list";
}
@Override
protected boolean isSingleDay() {
return true;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(ChartsHost.REFRESH)) {
if (ChartsHost.REFRESH.equals(action)) {
// TODO: use LimitLines to visualize smart alarms?
refresh();
} else {
@ -144,6 +148,7 @@ public class ActivityListingChartFragment extends AbstractActivityChartFragment<
return;
}
//noinspection RedundantIfStatement
if (mcd.getStepSessions().toArray().length == 0) {
getChartsHost().enableSwipeRefresh(true); //enable pull to refresh, might be needed
} else {
@ -184,15 +189,14 @@ public class ActivityListingChartFragment extends AbstractActivityChartFragment<
private void showOngoingActivitySnackbar(ActivitySession ongoingSession) {
String distanceLabel = stepListAdapter.getDistanceLabel(ongoingSession);
String stepLabel = stepListAdapter.getStepLabel(ongoingSession);
String durationLabel = stepListAdapter.getDurationLabel(ongoingSession);
String hrLabel = stepListAdapter.getHrLabel(ongoingSession);
String activityName = stepListAdapter.getActivityName(ongoingSession);
int icon = stepListAdapter.getIcon(ongoingSession);
String distanceLabel = FormatUtils.getFormattedDistanceLabel(ongoingSession.getDistance());
String stepLabel = String.valueOf(ongoingSession.getActiveSteps());
String durationLabel = DateTimeUtils.formatDurationHoursMinutes(ongoingSession.getEndTime().getTime() - ongoingSession.getStartTime().getTime(), TimeUnit.MILLISECONDS);
String hrLabel = String.valueOf(ongoingSession.getHeartRateAverage());
String activityName = ongoingSession.getActivityKind().getLabel(requireContext());
int icon = ongoingSession.getActivityKind().getIcon();
String text = String.format("%s:\u00A0%s, %s:\u00A0%s, %s:\u00A0%s, %s:\u00A0%s", activityName, durationLabel, getString(R.string.heart_rate), hrLabel, getString(R.string.steps), stepLabel, getString(R.string.distance), distanceLabel);
final Snackbar snackbar = Snackbar.make(rootView, text, 1000 * 8);
View snackbarView = snackbar.getView();

View File

@ -28,6 +28,7 @@ import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
@ -37,6 +38,7 @@ import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -49,7 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialDialogFragment;
public class ActivityListingDashboard extends MaterialDialogFragment {
@ -82,13 +84,11 @@ public class ActivityListingDashboard extends MaterialDialogFragment {
Bundle savedInstanceState) {
return inflater.inflate(R.layout.activity_list_total_dashboard, container);
}
@Override
public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
int time = getArguments().getInt("time", 1);
@ -106,10 +106,10 @@ public class ActivityListingDashboard extends MaterialDialogFragment {
}
stepListAdapter = new ActivityListingAdapter(getContext());
final TextView battery_status_date_from_text = (TextView) getView().findViewById(R.id.battery_status_date_from_text);
final TextView battery_status_date_to_text = (TextView) getView().findViewById(R.id.battery_status_date_to_text);
LinearLayout battery_status_date_to_layout = (LinearLayout) getView().findViewById(R.id.battery_status_date_to_layout);
final SeekBar battery_status_time_span_seekbar = (SeekBar) getView().findViewById(R.id.battery_status_time_span_seekbar);
final TextView battery_status_date_from_text = getView().findViewById(R.id.battery_status_date_from_text);
final TextView battery_status_date_to_text = getView().findViewById(R.id.battery_status_date_to_text);
LinearLayout battery_status_date_to_layout = getView().findViewById(R.id.battery_status_date_to_layout);
final SeekBar battery_status_time_span_seekbar = getView().findViewById(R.id.battery_status_time_span_seekbar);
boolean activity_list_debug_extra_time_range_value = GBApplication.getPrefs().getPreferences().getBoolean("activity_list_debug_extra_time_range", false);
@ -161,7 +161,7 @@ public class ActivityListingDashboard extends MaterialDialogFragment {
battery_status_time_span_text.setText(text);
battery_status_date_from_text.setText(DateTimeUtils.formatDate(new Date(timeFrom * 1000L)));
battery_status_date_to_text.setText(DateTimeUtils.formatDate(new Date(timeTo * 1000L)));
createRefreshTask("Visualizing data", getActivity()).execute();
createRefreshTask("Visualizing step sessions", getActivity()).execute();
}
@Override
@ -236,8 +236,12 @@ public class ActivityListingDashboard extends MaterialDialogFragment {
}
void indicate_progress(boolean inProgress) {
LinearLayout activity_list_dashboard_results_layout = getView().findViewById(R.id.activity_list_dashboard_results_layout);
RelativeLayout activity_list_dashboard_loading_layout = getView().findViewById(R.id.activity_list_dashboard_loading_layout);
View view = getView();
if (view == null) {
return;
}
LinearLayout activity_list_dashboard_results_layout = view.findViewById(R.id.activity_list_dashboard_results_layout);
RelativeLayout activity_list_dashboard_loading_layout = view.findViewById(R.id.activity_list_dashboard_loading_layout);
if (inProgress) {
activity_list_dashboard_results_layout.setVisibility(View.GONE);
activity_list_dashboard_loading_layout.setVisibility(View.VISIBLE);
@ -248,43 +252,49 @@ public class ActivityListingDashboard extends MaterialDialogFragment {
}
void populateData(ActivitySession item) {
TextView stepLabel = getView().findViewById(R.id.line_layout_step_label);
TextView stepTotalLabel = getView().findViewById(R.id.line_layout_total_step_label);
TextView distanceLabel = getView().findViewById(R.id.line_layout_distance_label);
TextView durationLabel = getView().findViewById(R.id.line_layout_duration_label);
TextView sessionCountLabel = getView().findViewById(R.id.line_layout_count_label);
LinearLayout durationLayout = getView().findViewById(R.id.line_layout_duration);
LinearLayout countLayout = getView().findViewById(R.id.line_layout_count);
LinearLayout stepsLayout = getView().findViewById(R.id.line_layout_step);
LinearLayout stepsTotalLayout = getView().findViewById(R.id.line_layout_total_step);
LinearLayout distanceLayout = getView().findViewById(R.id.line_layout_distance);
View view = getView();
if (view == null) {
return;
}
stepLabel.setText(stepListAdapter.getStepLabel(item));
stepTotalLabel.setText(stepListAdapter.getStepTotalLabel(item));
distanceLabel.setText(stepListAdapter.getDistanceLabel(item));
durationLabel.setText(stepListAdapter.getDurationLabel(item));
sessionCountLabel.setText(stepListAdapter.getSessionCountLabel(item));
TextView stepLabel = view.findViewById(R.id.line_layout_step_label);
TextView stepTotalLabel = view.findViewById(R.id.line_layout_total_step_label);
TextView distanceLabel = view.findViewById(R.id.line_layout_distance_label);
TextView durationLabel = view.findViewById(R.id.line_layout_duration_label);
TextView sessionCountLabel = view.findViewById(R.id.line_layout_count_label);
LinearLayout durationLayout = view.findViewById(R.id.line_layout_duration);
LinearLayout countLayout = view.findViewById(R.id.line_layout_count);
LinearLayout stepsLayout = view.findViewById(R.id.line_layout_step);
LinearLayout stepsTotalLayout = view.findViewById(R.id.line_layout_total_step);
LinearLayout distanceLayout = view.findViewById(R.id.line_layout_distance);
if (!stepListAdapter.hasDistance(item)) {
distanceLayout.setVisibility(View.GONE);
} else {
final long duration = item.getEndTime().getTime() - item.getStartTime().getTime();
durationLabel.setText(DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS));
sessionCountLabel.setText(String.valueOf(item.getSessionCount()));
if (item.getDistance() > 0) {
distanceLabel.setText(FormatUtils.getFormattedDistanceLabel(item.getDistance()));
distanceLayout.setVisibility(View.VISIBLE);
} else {
distanceLayout.setVisibility(View.GONE);
}
if (!stepListAdapter.hasSteps(item)) {
stepsLayout.setVisibility(View.GONE);
} else {
if (item.getActiveSteps() > 0) {
stepLabel.setText(String.valueOf(item.getActiveSteps()));
stepsLayout.setVisibility(View.VISIBLE);
} else {
stepsLayout.setVisibility(View.GONE);
}
if (!stepListAdapter.hasTotalSteps(item)) {
stepsTotalLayout.setVisibility(View.GONE);
countLayout.setVisibility(View.GONE);
durationLayout.setVisibility(View.GONE);
} else {
if (item.getTotalDaySteps() > 0) {
stepTotalLabel.setText(String.valueOf(item.getTotalDaySteps()));
stepsTotalLayout.setVisibility(View.VISIBLE);
countLayout.setVisibility(View.VISIBLE);
durationLayout.setVisibility(View.VISIBLE);
} else {
stepsTotalLayout.setVisibility(View.GONE);
countLayout.setVisibility(View.GONE);
durationLayout.setVisibility(View.GONE);
}
}

View File

@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityListItem;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
public class ActivityListingDetail extends DialogFragment {
@ -85,6 +86,21 @@ public class ActivityListingDetail extends DialogFragment {
ActivityListingAdapter stepListAdapter = new ActivityListingAdapter(getContext());
View activityItem = view.findViewById(R.id.activityItemHolder);
stepListAdapter.fill_item(item, 0, activityItem, null);
ActivityListItem activityListItem = new ActivityListItem(activityItem);
activityListItem.update(
item.getStartTime(),
item.getEndTime(),
item.getActivityKind(),
null,
item.getActiveSteps(),
item.getDistance(),
item.getHeartRateAverage(),
item.getIntensity(),
item.getEndTime().getTime() - item.getStartTime().getTime(),
false,
null,
false,
false
);
}
}
}

View File

@ -17,227 +17,107 @@
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
/**
* Adapter for displaying generic ItemWithDetails instances.
*/
public abstract class AbstractActivityListingAdapter<T> extends ArrayAdapter<T> {
public abstract class AbstractActivityListingAdapter<T> extends RecyclerView.Adapter<AbstractActivityListingAdapter.AbstractActivityListingViewHolder<T>> {
private final Context context;
private final List<T> items;
private final int SESSION_SUMMARY = ActivitySession.SESSION_SUMMARY;
private int backgroundColor = 0;
private int alternateColor = 0;
private boolean zebraStripes = true;
private boolean showTime = true;
private final BitSet selectedItems = new BitSet();
private OnItemClickListener onItemSingleClickListener;
private OnItemClickListener onItemLongClickListener;
public AbstractActivityListingAdapter(Context context) {
this(context, new ArrayList<T>());
}
public AbstractActivityListingAdapter(Context context, List<T> items) {
super(context, 0, items);
this.context = context;
this.items = items;
alternateColor = getAlternateColor(context);
this.items = new ArrayList<T>();
}
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.alternate_row_background, typedValue, true);
return typedValue.data;
public void setOnItemClickListener(final OnItemClickListener onItemSingleClickListener) {
this.onItemSingleClickListener = onItemSingleClickListener;
}
public void setOnItemLongClickListener(final OnItemClickListener onItemLongClickListener) {
this.onItemLongClickListener = onItemLongClickListener;
}
public Context getContext() {
return context;
}
public BitSet getSelectedItems() {
return selectedItems;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
T item = getItem(position);
if (isSummary(item, position)) {
view = fill_dashboard(item, position, view, parent, context);
} else if (isEmptySession(item, position)) {
view = fill_empty(parent);
} else {
view = fill_item(item, position, view, parent);
}
return view;
public int getItemCount() {
return items.size();
}
public View fill_item(T item, int position, View view, ViewGroup parent) {
if (parent != null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.activity_list_item, parent, false);
}
TextView timeFrom = view.findViewById(R.id.line_layout_time_from);
TextView timeTo = view.findViewById(R.id.line_layout_time_to);
TextView activityName = view.findViewById(R.id.line_layout_activity_name);
TextView stepLabel = view.findViewById(R.id.line_layout_step_label);
TextView distanceLabel = view.findViewById(R.id.line_layout_distance_label);
TextView hrLabel = view.findViewById(R.id.line_layout_hr_label);
TextView intensityLabel = view.findViewById(R.id.line_layout_intensity_label);
TextView durationLabel = view.findViewById(R.id.line_layout_duration_label);
TextView dateLabel = view.findViewById(R.id.line_layout_date_label);
LinearLayout timeLayout = view.findViewById(R.id.line_layout_time);
LinearLayout hrLayout = view.findViewById(R.id.line_layout_hr);
LinearLayout stepsLayout = view.findViewById(R.id.line_layout_step);
LinearLayout distanceLayout = view.findViewById(R.id.line_layout_distance);
LinearLayout intensityLayout = view.findViewById(R.id.line_layout_intensity);
LinearLayout dateLayout = view.findViewById(R.id.line_layout_date);
LinearLayout list_item_subparent_layout = view.findViewById(R.id.list_item_subparent_layout);
RelativeLayout parentLayout = view.findViewById(R.id.list_item_parent_layout);
ImageView activityIcon = view.findViewById(R.id.line_layout_activity_icon);
ImageView gpsIcon = view.findViewById(R.id.line_layout_gps_icon);
timeFrom.setText(getTimeFrom(item));
timeTo.setText(getTimeTo(item));
activityName.setText(getActivityName(item));
stepLabel.setText(getStepLabel(item));
distanceLabel.setText(getDistanceLabel(item));
hrLabel.setText(getHrLabel(item));
intensityLabel.setText(getIntensityLabel(item));
durationLabel.setText(getDurationLabel(item));
dateLabel.setText(getDateLabel(item));
if (!hasHR(item)) {
hrLayout.setVisibility(View.GONE);
} else {
hrLayout.setVisibility(View.VISIBLE);
}
if (!hasIntensity(item)) {
intensityLayout.setVisibility(View.GONE);
} else {
intensityLayout.setVisibility(View.VISIBLE);
}
if (!hasDistance(item)) {
distanceLayout.setVisibility(View.GONE);
} else {
distanceLayout.setVisibility(View.VISIBLE);
}
if (!hasSteps(item)) {
stepsLayout.setVisibility(View.GONE);
} else {
stepsLayout.setVisibility(View.VISIBLE);
}
if (!hasDate(item)) {
dateLayout.setVisibility(View.GONE);
} else {
dateLayout.setVisibility(View.VISIBLE);
}
if (!showTime) {
timeLayout.setVisibility(View.GONE);
} else {
timeLayout.setVisibility(View.VISIBLE);
}
if (!hasGPS(item)) {
gpsIcon.setVisibility(View.GONE);
} else {
gpsIcon.setVisibility(View.VISIBLE);
}
activityIcon.setImageResource(getIcon(item));
if (zebraStripes && position % 2 != 0) {
parentLayout.setBackgroundColor(alternateColor);
}
return view;
public T getItem(final int position) {
return items.get(position);
}
private View fill_empty(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.activity_list_item, parent, false);
view.setVisibility(View.GONE);
return view;
public int getPosition(final T item) {
return items.indexOf(item);
}
protected abstract View fill_dashboard(T item, int position, View view, ViewGroup parent, Context context);
protected abstract String getDateLabel(T item);
protected abstract boolean hasGPS(T item);
protected abstract boolean hasDate(T item);
protected abstract String getTimeFrom(T item);
protected abstract String getTimeTo(T item);
protected abstract String getActivityName(T item);
protected abstract String getStepLabel(T item);
protected abstract String getDistanceLabel(T item);
protected abstract String getHrLabel(T item);
protected abstract String getIntensityLabel(T item);
protected abstract String getDurationLabel(T item);
protected abstract String getSpeedLabel(T item);
protected abstract String getSessionCountLabel(T item);
protected abstract boolean hasHR(T item);
protected abstract boolean hasIntensity(T item);
protected abstract boolean hasDistance(T item);
protected abstract boolean hasSteps(T item);
protected abstract boolean hasTotalSteps(T item);
protected abstract boolean isSummary(T item, int position);
protected abstract boolean isEmptySession(T item, int position);
protected abstract boolean isEmptySummary(T item);
protected abstract String getStepTotalLabel(T item);
public void setZebraStripes(boolean enable) {
zebraStripes = enable;
public boolean isSelected(final int position) {
return selectedItems.get(position);
}
public void setShowTime(boolean enable) {
showTime = enable;
@Override
public int getItemViewType(final int position) {
if (position == 0) {
// First item is always the dashboard
return 0;
} else if (position == getItemCount() - 1) {
// Last item is always an empty session (prevent overlap with fab)
return 1;
}
return 2;
}
@DrawableRes
protected abstract int getIcon(T item);
@NonNull
@Override
public AbstractActivityListingViewHolder<T> onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
switch (viewType) {
case 1: // empty
return new EmptyViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_item, parent, false));
}
throw new IllegalArgumentException("Unknown view type " + viewType);
}
@Override
public void onBindViewHolder(@NonNull final AbstractActivityListingViewHolder<T> holder, int position) {
holder.fill(position, getItem(position), isSelected(position));
if (position > 0 && position < getItemCount() - 1) {
if (onItemSingleClickListener != null) {
holder.itemView.setOnClickListener(v -> onItemSingleClickListener.onClick(position));
}
if (onItemLongClickListener != null) {
holder.itemView.setOnLongClickListener(v -> {
onItemLongClickListener.onClick(position);
return true;
});
}
}
}
public List<T> getItems() {
return items;
@ -249,13 +129,13 @@ public abstract class AbstractActivityListingAdapter<T> extends ArrayAdapter<T>
public void setItems(List<T> items, boolean notify) {
this.items.clear();
this.items.addAll(items);
this.selectedItems.clear();
if (notify) {
notifyDataSetChanged();
}
}
public void setActivityKindFilter(int activityKind) {
this.setActivityKindFilter(activityKind);
}
public void setDateFromFilter(long date) {
@ -267,9 +147,35 @@ public abstract class AbstractActivityListingAdapter<T> extends ArrayAdapter<T>
public void setNameContainsFilter(String name) {
}
public void setItemsFilter(List items) {
public void setItemsFilter(List<Long> items) {
}
public void setDeviceFilter(long device) {
}
public abstract static class AbstractActivityListingViewHolder<T> extends RecyclerView.ViewHolder {
public AbstractActivityListingViewHolder(@NonNull final View itemView) {
super(itemView);
}
public abstract void fill(int position, T item, final boolean selected);
}
public interface OnItemClickListener {
void onClick(int position);
}
public class EmptyViewHolder extends AbstractActivityListingViewHolder<T> {
final View rootView;
public EmptyViewHolder(@NonNull final View itemView) {
super(itemView);
this.rootView = itemView;
}
@Override
public void fill(final int position, final T item, final boolean selected) {
rootView.setVisibility(View.GONE);
}
}
}

View File

@ -18,14 +18,15 @@
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,7 +47,9 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityListItem;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
@ -54,6 +57,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesFilter.ALL_DEVICES;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<BaseActivitySummary> {
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesAdapter.class);
@ -64,7 +73,6 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
String nameContainsFilter;
List<Long> itemsFilter;
private int activityKindFilter;
private int backgroundColor = 0;
public ActivitySummariesAdapter(Context context, GBDevice device, int activityKindFilter, long dateFromFilter, long dateToFilter, String nameContainsFilter, long deviceFilter, List itemsFilter) {
super(context);
@ -111,7 +119,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
qb.where(
BaseActivitySummaryDao.Properties.EndTime.lt(new Date(dateToFilter)));
}
if (nameContainsFilter != null && nameContainsFilter.length() > 0) {
if (nameContainsFilter != null && !nameContainsFilter.isEmpty()) {
qb.where(
BaseActivitySummaryDao.Properties.Name.like("%" + nameContainsFilter + "%"));
}
@ -122,14 +130,35 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
List<BaseActivitySummary> allSummaries = new ArrayList<>();
allSummaries.add(new BaseActivitySummary());
allSummaries.addAll(qb.build().list());
// HACK: Populate json in a dummy summary, so stats load faster
BaseActivitySummary dashboardSummary = new BaseActivitySummary();
final List<BaseActivitySummary> summaries = qb.build().list();
dashboardSummary.setSummaryData(StatsContainer.from(
device.getDeviceCoordinator().getActivitySummaryParser(device, getContext()),
summaries
).toJson());
allSummaries.add(dashboardSummary); // dashboard
allSummaries.addAll(summaries);
allSummaries.add(new BaseActivitySummary()); // empty
setItems(allSummaries, true);
} catch (Exception e) {
GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e);
}
}
@NonNull
@Override
public AbstractActivityListingViewHolder<BaseActivitySummary> onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
switch (viewType) {
case 0: // dashboard
return new DashboardViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_summary_dashboard_item, parent, false));
case 2: // item
return new ActivityItemViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_item, parent, false));
}
return super.onCreateViewHolder(parent, viewType);
}
public void setActivityKindFilter(int filter) {
this.activityKindFilter = filter;
}
@ -146,7 +175,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
this.nameContainsFilter = name;
}
public void setItemsFilter(List items) {
public void setItemsFilter(List<Long> items) {
this.itemsFilter = items;
}
@ -158,240 +187,205 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
return this.activityKindFilter;
}
@Override
protected View fill_dashboard(BaseActivitySummary item, int position, View view, ViewGroup parent, Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.activity_summary_dashboard_item, parent, false);
public static class ActivityItemViewHolder extends AbstractActivityListingViewHolder<BaseActivitySummary> {
final View rootView;
final ActivityListItem activityListItem;
double durationSum = 0;
double caloriesBurntSum = 0;
double distanceSum = 0;
double activeSecondsSum = 0;
double firstItemDate = 0;
double lastItemDate = 0;
int activitiesCount = getCount() - 1;
int activityIcon = 0;
boolean activitySame = true;
TextView durationSumView = view.findViewById(R.id.summary_dashboard_layout_duration_label);
TextView caloriesBurntSumView = view.findViewById(R.id.summary_dashboard_layout_calories_label);
TextView distanceSumView = view.findViewById(R.id.summary_dashboard_layout_distance_label);
TextView activeSecondsSumView = view.findViewById(R.id.summary_dashboard_layout_active_duration_label);
TextView timeStartView = view.findViewById(R.id.summary_dashboard_layout_from_label);
TextView timeEndView = view.findViewById(R.id.summary_dashboard_layout_to_label);
TextView activitiesCountView = view.findViewById(R.id.summary_dashboard_layout_count_label);
TextView activityKindView = view.findViewById(R.id.summary_dashboard_layout_activity_label);
ImageView activityIconView = view.findViewById(R.id.summary_dashboard_layout_activity_icon);
ImageView activityIconBigView = view.findViewById(R.id.summary_dashboard_layout_big_activity_icon);
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
for (BaseActivitySummary sportitem : getItems()) {
if (sportitem.getStartTime() == null) continue; //first item is empty, for dashboard
if (firstItemDate == 0) firstItemDate = sportitem.getStartTime().getTime();
lastItemDate = sportitem.getEndTime().getTime();
durationSum += sportitem.getEndTime().getTime() - sportitem.getStartTime().getTime();
if (activityIcon == 0) {
activityIcon = sportitem.getActivityKind();
} else {
if (activityIcon != sportitem.getActivityKind()) {
activitySame = false;
}
}
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device, getContext());
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem);
ActivitySummaryData summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
if (summarySubdata != null) {
if (summarySubdata.has("caloriesBurnt")) {
caloriesBurntSum += summarySubdata.getNumber("caloriesBurnt", 0).doubleValue();
}
if (summarySubdata.has("distanceMeters")) {
distanceSum += summarySubdata.getNumber("distanceMeters", 0).doubleValue();
}
if (summarySubdata.has("activeSeconds")) {
activeSecondsSum += summarySubdata.getNumber("activeSeconds", 0).doubleValue();
}
}
public ActivityItemViewHolder(@NonNull final View itemView) {
super(itemView);
this.rootView = itemView;
this.activityListItem = new ActivityListItem(itemView);
}
DecimalFormat df = new DecimalFormat("#.##");
durationSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) durationSum, TimeUnit.MILLISECONDS)));
caloriesBurntSumView.setText(String.format("%s %s", (long) caloriesBurntSum, context.getString(R.string.calories_unit)));
distanceSumView.setText(String.format("%s %s", df.format(distanceSum / 1000), context.getString(R.string.km)));
distanceSumView.setText(FormatUtils.getFormattedDistanceLabel(distanceSum));
activeSecondsSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) activeSecondsSum, TimeUnit.SECONDS)));
activitiesCountView.setText(String.valueOf(activitiesCount));
String activityName = context.getString(R.string.activity_summaries_all_activities);
if (gettActivityKindFilter() != 0) {
ActivityKind activityKind = ActivityKind.fromCode(gettActivityKindFilter());
activityName = activityKind.getLabel(context);
activityIconView.setImageResource(activityKind.getIcon());
activityIconBigView.setImageResource(activityKind.getIcon());
} else {
if (activitySame) {
ActivityKind activityKind = ActivityKind.fromCode(activityIcon);
@Override
public void fill(final int position, final BaseActivitySummary summary, final boolean selected) {
final boolean hasGps;
if (summary.getGpxTrack() != null) {
hasGps = true;
} else if (summary.getSummaryData() != null && summary.getSummaryData().contains(ActivitySummaryEntries.INTERNAL_HAS_GPS)) {
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summary.getSummaryData());
hasGps = summaryData != null && summaryData.getBoolean(INTERNAL_HAS_GPS, false);
} else {
hasGps = false;
}
this.activityListItem.update(
null,
null,
ActivityKind.fromCode(summary.getActivityKind()),
summary.getName(),
-1,
-1,
-1,
-1,
summary.getEndTime().getTime() - summary.getStartTime().getTime(),
hasGps,
summary.getStartTime(),
position % 2 == 1,
selected
);
}
}
public class DashboardViewHolder extends AbstractActivityListingViewHolder<BaseActivitySummary> {
final TextView durationSumView;
final TextView caloriesBurntSumView;
final TextView distanceSumView;
final TextView activeSecondsSumView;
final TextView timeStartView;
final TextView timeEndView;
final TextView activitiesCountView;
final TextView activityKindView;
final ImageView activityIconView;
final ImageView activityIconBigView;
public DashboardViewHolder(@NonNull final View itemView) {
super(itemView);
durationSumView = itemView.findViewById(R.id.summary_dashboard_layout_duration_label);
caloriesBurntSumView = itemView.findViewById(R.id.summary_dashboard_layout_calories_label);
distanceSumView = itemView.findViewById(R.id.summary_dashboard_layout_distance_label);
activeSecondsSumView = itemView.findViewById(R.id.summary_dashboard_layout_active_duration_label);
timeStartView = itemView.findViewById(R.id.summary_dashboard_layout_from_label);
timeEndView = itemView.findViewById(R.id.summary_dashboard_layout_to_label);
activitiesCountView = itemView.findViewById(R.id.summary_dashboard_layout_count_label);
activityKindView = itemView.findViewById(R.id.summary_dashboard_layout_activity_label);
activityIconView = itemView.findViewById(R.id.summary_dashboard_layout_activity_icon);
activityIconBigView = itemView.findViewById(R.id.summary_dashboard_layout_big_activity_icon);
}
@Override
public void fill(final int position, final BaseActivitySummary summary, final boolean selected) {
int activitiesCount = getItemCount() - 2; // remove dashboard and end spacer
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
String summaryData = summary.getSummaryData();
final StatsContainer stats;
if (StringUtils.isNotBlank(summaryData)) {
stats = StatsContainer.fromJson(summaryData);
} else {
stats = StatsContainer.from(
coordinator.getActivitySummaryParser(device, getContext()),
getItems()
);
}
DecimalFormat df = new DecimalFormat("#.##");
durationSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) stats.durationSum, TimeUnit.MILLISECONDS)));
caloriesBurntSumView.setText(String.format("%s %s", (long) stats.caloriesBurntSum, getContext().getString(R.string.calories_unit)));
distanceSumView.setText(String.format("%s %s", df.format(stats.distanceSum / 1000), getContext().getString(R.string.km)));
distanceSumView.setText(FormatUtils.getFormattedDistanceLabel(stats.distanceSum));
activeSecondsSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) stats.activeSecondsSum, TimeUnit.SECONDS)));
activitiesCountView.setText(String.valueOf(activitiesCount));
String activityName = getContext().getString(R.string.activity_summaries_all_activities);
if (gettActivityKindFilter() != 0) {
ActivityKind activityKind = ActivityKind.fromCode(gettActivityKindFilter());
activityName = activityKind.getLabel(getContext());
activityIconView.setImageResource(activityKind.getIcon());
activityIconBigView.setImageResource(activityKind.getIcon());
} else if (stats.activityIcon != 0) {
ActivityKind activityKind = ActivityKind.fromCode(stats.activityIcon);
activityIconView.setImageResource(activityKind.getIcon());
activityIconBigView.setImageResource(activityKind.getIcon());
}
activityKindView.setText(activityName);
//start and end are inverted when filer not applied, because items are sorted the other way
timeStartView.setText((dateFromFilter != 0) ? DateTimeUtils.formatDate(new Date(dateFromFilter)) : DateTimeUtils.formatDate(new Date((long) stats.lastItemDate)));
timeEndView.setText((dateToFilter != 0) ? DateTimeUtils.formatDate(new Date(dateToFilter)) : DateTimeUtils.formatDate(new Date((long) stats.firstItemDate)));
}
activityKindView.setText(activityName);
//start and end are inverted when filer not applied, because items are sorted the other way
timeStartView.setText((dateFromFilter != 0) ? DateTimeUtils.formatDate(new Date(dateFromFilter)) : DateTimeUtils.formatDate(new Date((long) lastItemDate)));
timeEndView.setText((dateToFilter != 0) ? DateTimeUtils.formatDate(new Date(dateToFilter)) : DateTimeUtils.formatDate(new Date((long) firstItemDate)));
return view;
}
@Override
protected String getDateLabel(BaseActivitySummary item) {
Date startTime = item.getStartTime();
String separator = ",";
if (startTime != null) {
String activityDay;
private static class StatsContainer {
private static final Gson GSON = new GsonBuilder().create();
if (DateUtils.isToday(startTime.getTime())) {
activityDay = getContext().getString(R.string.activity_summary_today);
} else if (DateTimeUtils.isYesterday(startTime)) {
activityDay = getContext().getString(R.string.activity_summary_yesterday);
} else {
activityDay = DateTimeUtils.formatDate(startTime, DateUtils.FORMAT_SHOW_WEEKDAY);
private final double durationSum;
private final double caloriesBurntSum;
private final double distanceSum;
private final double activeSecondsSum;
private final double firstItemDate;
private final double lastItemDate;
private final int activityIcon;
public StatsContainer(final double durationSum,
final double caloriesBurntSum,
final double distanceSum,
final double activeSecondsSum,
final double firstItemDate,
final double lastItemDate,
final int activityIcon) {
this.durationSum = durationSum;
this.caloriesBurntSum = caloriesBurntSum;
this.distanceSum = distanceSum;
this.activeSecondsSum = activeSecondsSum;
this.firstItemDate = firstItemDate;
this.lastItemDate = lastItemDate;
this.activityIcon = activityIcon;
}
private static StatsContainer from(final ActivitySummaryParser summaryParser,
final List<BaseActivitySummary> activities) {
double durationSum = 0;
double caloriesBurntSum = 0;
double distanceSum = 0;
double activeSecondsSum = 0;
double firstItemDate = 0;
double lastItemDate = 0;
int activityIcon = 0;
boolean activitySame = true;
for (BaseActivitySummary summary : activities) {
if (summary.getStartTime() == null) continue; //first item is empty, for dashboard
if (firstItemDate == 0) firstItemDate = summary.getStartTime().getTime();
lastItemDate = summary.getEndTime().getTime();
durationSum += summary.getEndTime().getTime() - summary.getStartTime().getTime();
if (activityIcon == 0) {
activityIcon = summary.getActivityKind();
} else {
if (activityIcon != summary.getActivityKind()) {
activitySame = false;
}
}
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, summary);
ActivitySummaryData summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
if (summarySubdata != null) {
if (summarySubdata.has("caloriesBurnt")) {
caloriesBurntSum += summarySubdata.getNumber("caloriesBurnt", 0).doubleValue();
}
if (summarySubdata.has("distanceMeters")) {
distanceSum += summarySubdata.getNumber("distanceMeters", 0).doubleValue();
}
if (summarySubdata.has("activeSeconds")) {
activeSecondsSum += summarySubdata.getNumber("activeSeconds", 0).doubleValue();
}
}
}
String activityTime = DateTimeUtils.formatTime(startTime.getHours(), startTime.getMinutes());
return String.format("%s%s %s", activityDay, separator, activityTime);
}
return "Unknown time";
}
@Override
protected boolean hasGPS(BaseActivitySummary item) {
if (item.getGpxTrack() != null) {
return true;
} else {
return false;
}
}
@Override
protected boolean hasDate(BaseActivitySummary item) {
return true;
}
@Override
protected String getTimeFrom(BaseActivitySummary item) {
Date time = item.getStartTime();
return DateTimeUtils.formatTime(time.getHours(), time.getMinutes());
}
@Override
protected String getTimeTo(BaseActivitySummary item) {
Date time = item.getEndTime();
return DateTimeUtils.formatTime(time.getHours(), time.getMinutes());
}
@Override
protected String getActivityName(BaseActivitySummary item) {
String activityLabel = item.getName();
String separator = ",";
if (activityLabel == null) {
activityLabel = "";
separator = "";
return new StatsContainer(
durationSum,
caloriesBurntSum,
distanceSum,
activeSecondsSum,
firstItemDate,
lastItemDate,
activitySame ? activityIcon : 0
);
}
String activityKindName = ActivityKind.fromCode(item.getActivityKind()).getLabel(getContext());
return String.format("%s%s %s", activityKindName, separator, activityLabel);
}
public static StatsContainer fromJson(final String json) {
return GSON.fromJson(json, StatsContainer.class);
}
@Override
protected String getStepLabel(BaseActivitySummary item) {
return null;
}
@Override
protected String getDistanceLabel(BaseActivitySummary item) {
return null;
}
@Override
protected String getHrLabel(BaseActivitySummary item) {
return null;
}
@Override
protected String getIntensityLabel(BaseActivitySummary item) {
return null;
}
@Override
protected String getDurationLabel(BaseActivitySummary item) {
Long duration = item.getEndTime().getTime() - item.getStartTime().getTime();
return DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS);
}
@Override
protected String getSpeedLabel(BaseActivitySummary item) {
return null;
}
@Override
protected String getSessionCountLabel(BaseActivitySummary item) {
return "";
}
@Override
protected boolean hasHR(BaseActivitySummary item) {
return false;
}
@Override
protected boolean hasIntensity(BaseActivitySummary item) {
return false;
}
@Override
protected boolean hasDistance(BaseActivitySummary item) {
return false;
}
@Override
protected boolean hasSteps(BaseActivitySummary item) {
return false;
}
@Override
protected boolean hasTotalSteps(BaseActivitySummary item) {
return false;
}
@Override
protected boolean isSummary(BaseActivitySummary item, int position) {
return position == 0;
}
@Override
protected boolean isEmptySession(BaseActivitySummary item, int position) { return false; }
@Override
protected boolean isEmptySummary(BaseActivitySummary item) {
return false;
}
@Override
protected String getStepTotalLabel(BaseActivitySummary item) {
return null;
}
@Override
protected int getIcon(BaseActivitySummary item) {
return ActivityKind.fromCode(item.getActivityKind()).getIcon();
}
public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
public String toJson() {
return GSON.toJson(this);
}
}
}

View File

@ -0,0 +1,188 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateUtils;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.apache.commons.lang3.StringUtils;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
public class ActivityListItem {
private final View rootView;
private final TextView timeFromView;
private final TextView timeToView;
private final TextView activityName;
private final TextView stepLabel;
private final TextView distanceLabel;
private final TextView hrLabel;
private final TextView intensityLabel;
private final TextView durationLabel;
private final TextView dateLabel;
private final LinearLayout timeLayout;
private final LinearLayout hrLayout;
private final LinearLayout stepsLayout;
private final LinearLayout distanceLayout;
private final LinearLayout intensityLayout;
private final LinearLayout dateLayout;
private final RelativeLayout parentLayout;
private final ImageView activityIcon;
private final ImageView gpsIcon;
private final int backgroundColor;
private final int alternateColor;
private final int selectedColor;
public ActivityListItem(final View itemView) {
this.rootView = itemView;
this.timeFromView = itemView.findViewById(R.id.line_layout_time_from);
this.timeToView = itemView.findViewById(R.id.line_layout_time_to);
this.activityName = itemView.findViewById(R.id.line_layout_activity_name);
this.stepLabel = itemView.findViewById(R.id.line_layout_step_label);
this.distanceLabel = itemView.findViewById(R.id.line_layout_distance_label);
this.hrLabel = itemView.findViewById(R.id.line_layout_hr_label);
this.intensityLabel = itemView.findViewById(R.id.line_layout_intensity_label);
this.durationLabel = itemView.findViewById(R.id.line_layout_duration_label);
this.dateLabel = itemView.findViewById(R.id.line_layout_date_label);
this.timeLayout = itemView.findViewById(R.id.line_layout_time);
this.hrLayout = itemView.findViewById(R.id.line_layout_hr);
this.stepsLayout = itemView.findViewById(R.id.line_layout_step);
this.distanceLayout = itemView.findViewById(R.id.line_layout_distance);
this.intensityLayout = itemView.findViewById(R.id.line_layout_intensity);
this.dateLayout = itemView.findViewById(R.id.line_layout_date);
this.parentLayout = itemView.findViewById(R.id.list_item_parent_layout);
this.activityIcon = itemView.findViewById(R.id.line_layout_activity_icon);
this.gpsIcon = itemView.findViewById(R.id.line_layout_gps_icon);
this.backgroundColor = 0;
this.alternateColor = getThemedColor(itemView.getContext(), R.attr.alternate_row_background);
this.selectedColor = ContextCompat.getColor(itemView.getContext(), R.color.accent);
}
public void update(@Nullable final Date timeFrom,
@Nullable final Date timeTo,
final ActivityKind activityKind,
@Nullable final String activityLabel,
final int steps,
final float distance,
final int heartRate,
final float intensity,
final long duration,
final boolean hasGps,
@Nullable final Date date,
final boolean zebraStripe,
final boolean selected) {
final String activityKindLabel = activityKind.getLabel(activityName.getContext());
if (StringUtils.isNotBlank(activityLabel)) {
activityName.setText(String.format("%s, %s", activityKindLabel, activityLabel));
} else {
activityName.setText(activityKindLabel);
}
durationLabel.setText(DateTimeUtils.formatDurationHoursMinutes(duration, TimeUnit.MILLISECONDS));
if (heartRate > 0) {
hrLabel.setText(String.valueOf(heartRate));
hrLayout.setVisibility(View.VISIBLE);
} else {
hrLayout.setVisibility(View.GONE);
}
if (intensity >= 0) {
final DecimalFormat df = new DecimalFormat("###");
intensityLabel.setText(df.format(intensity));
intensityLayout.setVisibility(View.VISIBLE);
} else {
intensityLayout.setVisibility(View.GONE);
}
if (distance > 0) {
distanceLabel.setText(FormatUtils.getFormattedDistanceLabel(distance));
distanceLayout.setVisibility(View.VISIBLE);
} else {
distanceLayout.setVisibility(View.GONE);
}
if (steps > 0) {
stepLabel.setText(String.valueOf(steps));
stepsLayout.setVisibility(View.VISIBLE);
} else {
stepsLayout.setVisibility(View.GONE);
}
if (date != null) {
dateLabel.setText(formatDate(date));
dateLayout.setVisibility(View.VISIBLE);
} else {
dateLayout.setVisibility(View.GONE);
}
if (timeFrom != null && timeTo != null) {
timeFromView.setText(DateTimeUtils.formatTime(timeFrom.getHours(), timeFrom.getMinutes()));
timeToView.setText(DateTimeUtils.formatTime(timeTo.getHours(), timeTo.getMinutes()));
timeLayout.setVisibility(View.VISIBLE);
} else {
timeLayout.setVisibility(View.GONE);
}
if (hasGps) {
gpsIcon.setVisibility(View.VISIBLE);
} else {
gpsIcon.setVisibility(View.GONE);
}
activityIcon.setImageResource(activityKind.getIcon());
if (parentLayout != null) {
if (selected) {
parentLayout.setBackgroundColor(selectedColor);
} else if (zebraStripe) {
parentLayout.setBackgroundColor(alternateColor);
} else {
parentLayout.setBackgroundColor(backgroundColor);
}
}
}
private String formatDate(final Date date) {
if (date != null) {
final String activityDay;
if (DateUtils.isToday(date.getTime())) {
activityDay = rootView.getContext().getString(R.string.activity_summary_today);
} else if (DateTimeUtils.isYesterday(date)) {
activityDay = rootView.getContext().getString(R.string.activity_summary_yesterday);
} else {
activityDay = DateTimeUtils.formatDate(date, DateUtils.FORMAT_SHOW_WEEKDAY);
}
final String activityTime = DateTimeUtils.formatTime(date.getHours(), date.getMinutes());
return String.format("%s, %s", activityDay, activityTime);
}
return rootView.getContext().getString(R.string.unknown);
}
public static int getThemedColor(Context context, int resid) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(resid, typedValue, true);
return typedValue.data;
}
}

View File

@ -44,7 +44,8 @@ public class ActivitySummaryItems {
}
public BaseActivitySummary getNextItem() {
if (current_position + 1 < itemsAdapter.getCount()) {
// last one is empty to avoid items behind fab
if (current_position + 2 < itemsAdapter.getItemCount()) {
current_position += 1;
return itemsAdapter.getItem(current_position);
}

View File

@ -0,0 +1,87 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
public final class ActivitySummaryUtils {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryUtils.class);
private ActivitySummaryUtils() {
// utility class
}
@Nullable
public static File getTrackFile(final BaseActivitySummary summary) {
final String gpxTrack = summary.getGpxTrack();
if (gpxTrack != null) {
return FileUtils.tryFixPath(new File(gpxTrack));
}
final String rawDetails = summary.getRawDetailsPath();
if (rawDetails != null && rawDetails.endsWith(".fit")) {
return FileUtils.tryFixPath(new File(rawDetails));
}
return null;
}
@Nullable
public static File getGpxFile(final BaseActivitySummary summary) {
final File trackFile = getTrackFile(summary);
if (trackFile == null) {
return null;
}
try {
if (trackFile.getName().endsWith(".gpx")) {
return trackFile;
} else if (trackFile.getName().endsWith(".fit")) {
return convertFitToGpx(summary, trackFile);
} else {
LOG.error("Unknown track format for {}", trackFile.getName());
}
} catch (final Exception e) {
LOG.error("Failed to get gpx track", e);
}
return null;
}
private static File convertFitToGpx(final BaseActivitySummary summary, final File file) throws IOException, ActivityTrackExporter.GPXTrackEmptyException {
final FitFile fitFile = FitFile.parseIncoming(file);
final List<ActivityPoint> activityPoints = fitFile.getRecords().stream()
.filter(r -> r instanceof FitRecord)
.map(r -> ((FitRecord) r).toActivityPoint())
.filter(ap -> ap.getLocation() != null)
.collect(Collectors.toList());
final ActivityTrack activityTrack = new ActivityTrack();
activityTrack.setName(summary.getName());
activityTrack.addTrackPoints(activityPoints);
final File cacheDir = GBApplication.getContext().getCacheDir();
final File rawCacheDir = new File(cacheDir, "gpx");
//noinspection ResultOfMethodCallIgnored
rawCacheDir.mkdir();
final File gpxFile = new File(rawCacheDir, file.getName().replace(".fit", ".gpx"));
final GPXExporter gpxExporter = new GPXExporter();
gpxExporter.performExport(activityTrack, gpxFile);
return gpxFile;
}
}

View File

@ -23,7 +23,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemListView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

View File

@ -12,18 +12,13 @@
android:id="@+id/list_item_subparent_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/list_selector"
android:paddingStart="5dp"
android:paddingTop="5dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/line_layout_time"
android:layout_width="wrap_content"
@ -37,7 +32,7 @@
android:layout_gravity="end"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="14:30" />
android:text="@string/time_empty_value" />
<TextView
android:id="@+id/line_layout_time_to"
@ -48,7 +43,7 @@
android:gravity="bottom"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="16:30" />
android:text="@string/time_empty_value" />
</LinearLayout>
@ -65,7 +60,7 @@
android:layout_marginBottom="4dp"
android:contentDescription="@string/candidate_item_device_image"
android:padding="8dp"
app:srcCompat="@drawable/ic_activity_running" />
app:srcCompat="@drawable/ic_activity_unknown" />
</LinearLayout>
@ -80,7 +75,7 @@
android:layout_height="wrap_content"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="Running"
android:text="@string/unknown"
android:textStyle="bold" />
<LinearLayout
@ -101,7 +96,7 @@
android:gravity="end"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="25min" />
android:text="@string/stats_empty_value" />
<ImageView
android:id="@+id/line_layout_gps_icon"
@ -146,7 +141,7 @@
android:gravity="start"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15000" />
android:text="@string/stats_empty_value" />
</LinearLayout>
@ -176,7 +171,7 @@
android:gravity="start"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="101" />
android:text="@string/stats_empty_value" />
</LinearLayout>
@ -206,7 +201,7 @@
android:gravity="start"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15.1km" />
android:text="@string/stats_empty_value" />
</LinearLayout>
@ -236,7 +231,7 @@
android:gravity="start"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="122" />
android:text="@string/stats_empty_value" />
</LinearLayout>
@ -266,7 +261,7 @@
android:gravity="start"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="1.1.1973" />
android:text="@string/stats_empty_value" />
</LinearLayout>
@ -277,6 +272,4 @@
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -16,7 +16,7 @@
android:textStyle="bold" />
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -24,7 +24,7 @@
android:layout_alignParentBottom="false"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp">
</ListView>
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@ -41,7 +41,7 @@
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15000"
android:text="@string/stats_empty_value"
android:textSize="24sp" />
</LinearLayout>

View File

@ -41,7 +41,7 @@
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15"
android:text="@string/stats_empty_value"
android:textSize="24sp" />
</LinearLayout>

View File

@ -41,7 +41,7 @@
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15.1km"
android:text="@string/stats_empty_value"
android:textSize="24sp" />
</LinearLayout>

View File

@ -41,7 +41,7 @@
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="122"
android:text="@string/stats_empty_value"
android:textSize="24sp" />
</LinearLayout>

View File

@ -41,7 +41,7 @@
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15000"
android:text="@string/stats_empty_value"
android:textSize="24sp" />
</LinearLayout>

View File

@ -1011,6 +1011,7 @@
<string name="sleep_colored_stats_rem_avg">REM AVG</string>
<string name="sleep_colored_stats_awake_avg">Awake AVG</string>
<string name="stats_empty_value">-</string>
<string name="time_empty_value" translatable="false">--:--</string>
<string name="stats_lowest_hr">Lowest HR</string>
<string name="stats_highest_hr">Highest HR</string>
<string name="transition">Transition</string>
@ -3264,6 +3265,7 @@
<string name="folder_is_empty">Folder is empty</string>
<string name="folder">Folder</string>
<string name="url">URL</string>
<string name="number_selected_items">%1d selected</string>
<string name="garmin_agps_local_file">Local file</string>
<string name="pref_garmin_agps_help">The list below contains all URLs requested by the watch for AGPS updates. You can select a file from the phone\'s storage that will be sent to the watch when it requests an update.</string>
<string name="copied_to_clipboard">Copied to clipboard</string>