diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 9e32f6738..5c5284ea1 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -82,6 +82,9 @@ limitations under the License.
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/OpenTracksTheme">
+
diff --git a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java
index 1f58a7b78..fc1f41b8e 100644
--- a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java
@@ -32,13 +32,16 @@
import java.io.File;
import java.time.Duration;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
-
+import java.util.Date;
+import java.text.SimpleDateFormat;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
@@ -267,6 +270,21 @@ public Track getTrack(@NonNull Track.Id trackId) {
}
return null;
}
+ public Track getTrack(@NonNull Date date) {
+ long millisecondsSinceEpoch = date.getTime();
+ LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ LocalDate modifiedLocalDate = localDate.plusDays(1);
+ Date modifiedDate = Date.from(modifiedLocalDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ long millisNextDaySinceEpoch = modifiedDate.getTime();
+
+ String query = TracksColumns.STARTTIME + ">? AND " + TracksColumns.STARTTIME + "<=?";
+ try (Cursor cursor = getTrackCursor(query, new String[]{Long.toString(millisecondsSinceEpoch), Long.toString(millisNextDaySinceEpoch)}, TracksColumns._ID)) {
+ if (cursor != null && cursor.moveToNext()) {
+ return createTrack(cursor);
+ }
+ }
+ return null;
+ }
public Track getTrack(@NonNull UUID trackUUID) {
String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
diff --git a/src/main/java/de/dennisguse/opentracks/data/models/TrackSegment.java b/src/main/java/de/dennisguse/opentracks/data/models/TrackSegment.java
new file mode 100644
index 000000000..f0800ee34
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/data/models/TrackSegment.java
@@ -0,0 +1,86 @@
+package de.dennisguse.opentracks.data.models;
+
+import androidx.annotation.NonNull;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Optional;
+
+public class TrackSegment {
+
+ private static final String TAG = TrackSegment.class.getSimpleName();
+ private TrackPoint.Id id;
+ @NonNull
+ private final Instant time;
+
+ private List trackPoints;
+
+ public TrackSegment(@NonNull Instant time) {
+ this.time = time;
+ trackPoints = new ArrayList<>();
+ }
+
+ public void addTrackPoint(TrackPoint point) {
+ this.trackPoints.add(point);
+ }
+
+ public int getTrackPointCount() {
+ return trackPoints.size();
+ }
+
+ public Boolean hasTrackPoints() {
+ return !trackPoints.isEmpty();
+ }
+
+ public double getInitialElevation() {
+ Optional firstPoint = trackPoints.stream().findFirst();
+ if (firstPoint.isPresent()) {
+ TrackPoint point = firstPoint.get();
+ return point.getAltitude().toM();
+ }
+ return 0;
+ }
+
+ public long getDisplacement() {
+ long displacement = 0;
+ for (TrackPoint point: trackPoints) {
+ if (point.hasAltitudeGain()) {
+ displacement += point.getAltitudeGain();
+ }
+
+ if (point.hasAltitudeLoss()) {
+ displacement += point.getAltitudeLoss();
+ }
+ }
+
+ return displacement;
+ }
+ public Distance getDistance() {
+ if (trackPoints == null) {
+ return null;
+ }
+ TrackPoint first = trackPoints.get(0);
+ TrackPoint last = trackPoints.get(trackPoints.size() - 1);
+ return last.distanceToPrevious(first);
+ }
+
+ public Duration getTotalTime(){
+
+ if(trackPoints == null){
+ return null;
+ }
+ TrackPoint startTime = trackPoints.get(0);
+ TrackPoint endTime = trackPoints.get(trackPoints.size() - 1);
+
+ return Duration.between(startTime.getTime(), endTime.getTime());
+ }
+
+ public double getSpeed(){
+ // in m/s
+ double totalDistance = getDistance().toM();
+ long totalTime = getTotalTime().toSeconds();
+ return totalDistance/totalTime;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java
index 949466bdc..9691930bf 100644
--- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java
+++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java
@@ -22,6 +22,7 @@
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.aggregatedStatistics.SeasonStats.SeasonStatActivity;
+import de.dennisguse.opentracks.ui.aggregatedStatistics.daySpecificStats.DaySpecificActivity;
import de.dennisguse.opentracks.util.StringUtils;
public class AggregatedStatisticsAdapter extends RecyclerView.Adapter {
@@ -93,22 +94,32 @@ public void onClick(View v) {
}
});
- viewBinding.runsAndLiftsButton.setOnClickListener(new View.OnClickListener(){
+ viewBinding.runsAndLiftsButton.setOnClickListener(new View.OnClickListener() {
@Override
- public void onClick(View v){
- Intent intent=new Intent(v.getContext(), CalendarActivity.class);
+ public void onClick(View v) {
+ Intent intent = new Intent(v.getContext(), CalendarActivity.class);
intent.putExtra("Display Fields", "Runs and Lifts");
v.getContext().startActivity(intent);
}
});
- viewBinding.elevationAndSpeedButton.setOnClickListener(new View.OnClickListener(){
+ viewBinding.elevationAndSpeedButton.setOnClickListener(new View.OnClickListener() {
@Override
- public void onClick(View v){
- Intent intent=new Intent(v.getContext(), CalendarActivity.class);
+ public void onClick(View v) {
+ Intent intent = new Intent(v.getContext(), CalendarActivity.class);
intent.putExtra("Display Fields", "Elevation and Speed");
v.getContext().startActivity(intent);
}
});
+
+ viewBinding.calendarButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Context context = viewBinding.getRoot().getContext();
+ Intent intent = new Intent(context, DaySpecificActivity.class);
+ context.startActivity(intent);
+ }
+ });
}
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificActivity.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificActivity.java
new file mode 100644
index 000000000..6007514ae
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificActivity.java
@@ -0,0 +1,136 @@
+package de.dennisguse.opentracks.ui.aggregatedStatistics.daySpecificStats;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import de.dennisguse.opentracks.AbstractActivity;
+import de.dennisguse.opentracks.R;
+import de.dennisguse.opentracks.TrackListActivity;
+import de.dennisguse.opentracks.data.ContentProviderUtils;
+import de.dennisguse.opentracks.data.TrackPointIterator;
+import de.dennisguse.opentracks.data.models.Track;
+import de.dennisguse.opentracks.data.models.TrackPoint;
+import de.dennisguse.opentracks.data.models.TrackSegment;
+import de.dennisguse.opentracks.databinding.DaySpecificActivityBinding;
+
+import java.time.format.DateTimeFormatter;
+import java.time.LocalDate;
+import java.util.Date;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.ArrayList;
+
+public class DaySpecificActivity extends AbstractActivity {
+
+ private DaySpecificActivityBinding viewBinding;
+ private static final String TAG = DaySpecificActivity.class.getSimpleName();
+ public static final String EXTRA_TRACK_DATE = "track_date";
+ private Date activityDate;
+ private ContentProviderUtils contentProviderUtils;
+ private Track.Id trackId;
+ private List trackSegments;
+ private DaySpecificAdapter dataAdapter;
+
+ private String fallbackDate = "2024-03-09";
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ trackSegments = new ArrayList<>();
+ contentProviderUtils = new ContentProviderUtils(this);
+ handleIntent(getIntent());
+ updateTrackSegments();
+ setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
+
+ dataAdapter = new DaySpecificAdapter(this, viewBinding.segmentList);
+ dataAdapter.swapData(trackSegments);
+ viewBinding.segmentList.setAdapter(dataAdapter);
+ viewBinding.segmentListToolbar.setTitle(fallbackDate);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ updateTrackSegments();
+ dataAdapter.swapData(trackSegments);
+ }
+
+ public void updateTrackSegments() {
+ if (trackId == null) {
+ return;
+ }
+ try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
+ TrackSegment currentSegment = null;
+ while (trackPointIterator.hasNext()) {
+ TrackPoint nextPoint = trackPointIterator.next();
+
+ switch (nextPoint.getType()) {
+ case SEGMENT_START_AUTOMATIC:
+ case SEGMENT_START_MANUAL:
+ if (currentSegment != null) {
+ trackSegments.add(currentSegment);
+ }
+ currentSegment = new TrackSegment(nextPoint.getTime());
+ break;
+
+ case SEGMENT_END_MANUAL:
+ trackSegments.add(currentSegment);
+ currentSegment = null;
+
+ case TRACKPOINT:
+ if (currentSegment != null) {
+ currentSegment.addTrackPoint(nextPoint);
+ }
+ break;
+
+ default:
+ Log.d(TAG, "No Action for TrackPoint IDLE/SENSORPOINT while recording segments");
+ }
+ }
+ }
+ }
+
+ private void showNoTracksFoundToast() {
+ finish();
+ Toast.makeText(DaySpecificActivity.this, "No Tracks found for date: " + fallbackDate + "\n Please import GPX file from Moodle", Toast.LENGTH_LONG).show();
+ }
+ private Date getFallbackDate() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ LocalDate localDate = LocalDate.parse(fallbackDate, formatter);
+ return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ }
+
+ private void handleIntent(Intent intent) {
+ Date dateFromCalendar = intent.getParcelableExtra(EXTRA_TRACK_DATE);
+ if (dateFromCalendar == null) {
+ Log.e(TAG, DaySpecificActivity.class.getSimpleName() + " needs EXTRA_TRACK_ID.");
+
+ // None provided, we will assume a specific date on our own
+ activityDate = getFallbackDate();
+ } else {
+ activityDate = dateFromCalendar;
+ }
+
+ Track track = contentProviderUtils.getTrack(activityDate);
+ if (track == null) {
+ showNoTracksFoundToast();
+ } else {
+ trackId = track.getId();
+ }
+ }
+
+ @Override
+ protected View getRootView() {
+ viewBinding = DaySpecificActivityBinding.inflate(getLayoutInflater());
+ return viewBinding.getRoot();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificAdapter.java
new file mode 100644
index 000000000..00e883a18
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/daySpecificStats/DaySpecificAdapter.java
@@ -0,0 +1,139 @@
+package de.dennisguse.opentracks.ui.aggregatedStatistics.daySpecificStats;
+
+import android.database.Cursor;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.view.ActionMode;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+import de.dennisguse.opentracks.R;
+import de.dennisguse.opentracks.data.models.Distance;
+import de.dennisguse.opentracks.data.models.Track;
+import de.dennisguse.opentracks.data.models.TrackSegment;
+import de.dennisguse.opentracks.databinding.DaySpecificActivityItemBinding;
+import de.dennisguse.opentracks.ui.util.ActivityUtils;
+
+public class DaySpecificAdapter extends RecyclerView.Adapter implements ActionMode.Callback {
+
+ private static final String TAG = DaySpecificAdapter.class.getSimpleName();
+ DaySpecificActivityItemBinding viewBinding;
+ private final AppCompatActivity context;
+ private final RecyclerView recyclerView;
+
+ private final SparseBooleanArray selection = new SparseBooleanArray();
+
+ private Cursor cursor;
+
+ private ActivityUtils.ContextualActionModeCallback actionModeCallback;
+ private List trackSegments;
+
+ public DaySpecificAdapter(AppCompatActivity context, RecyclerView recyclerView) {
+ this.context = context;
+ this.recyclerView = recyclerView;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.day_specific_activity_item, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ DaySpecificAdapter.ViewHolder viewHolder = (DaySpecificAdapter.ViewHolder) holder;
+ TrackSegment segment = trackSegments.get(position);
+ viewHolder.bind(segment);
+ }
+
+ public void swapData(List segments) {
+ this.trackSegments = segments;
+ this.notifyDataSetChanged();
+ }
+ @Override
+ public int getItemCount() {
+ return trackSegments.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
+
+ private final DaySpecificActivityItemBinding viewBinding;
+ private final View view;
+
+ private Track.Id trackId;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ viewBinding = DaySpecificActivityItemBinding.bind(itemView);
+ view = itemView;
+
+ view.setOnClickListener(this);
+ view.setOnLongClickListener(this);
+ }
+
+ public void bind(TrackSegment segment) {
+ double speed = segment.getSpeed();
+ String formattedSpeed = String.format("%.2f", speed);
+
+ double distance = segment.getDistance().toM();
+ String formattedDistance = String.format("%.2f", distance);
+
+ double elevation = segment.getInitialElevation();
+ String formattedElevation = String.format("%.1f", elevation);
+
+ viewBinding.daySpecificActivityDisplacement.setText(formattedElevation + " m");
+ viewBinding.daySpecificActivityDistance.setText(formattedDistance + " mts");
+ viewBinding.daySpecificActivitySpeed.setText(formattedSpeed + " m/s");
+ viewBinding.daySpecificActivityTime.setText(segment.getTotalTime().toMinutes() + " minutes");
+ }
+
+ public void setSelected(boolean isSelected) {
+ selection.put((int) getId(), isSelected);
+ view.setActivated(isSelected);
+ }
+
+ public long getId() {
+ return trackId.id();
+ }
+
+ @Override
+ public void onClick(View view) {
+
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/res/layout/aggregated_stats_list_item.xml b/src/main/res/layout/aggregated_stats_list_item.xml
index 9d5eeae43..5b5b50fe4 100644
--- a/src/main/res/layout/aggregated_stats_list_item.xml
+++ b/src/main/res/layout/aggregated_stats_list_item.xml
@@ -244,4 +244,32 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/aggregated_stats_horizontal_line" />
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/day_specific_activity.xml b/src/main/res/layout/day_specific_activity.xml
new file mode 100644
index 000000000..ad3d3da38
--- /dev/null
+++ b/src/main/res/layout/day_specific_activity.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/day_specific_activity_item.xml b/src/main/res/layout/day_specific_activity_item.xml
new file mode 100644
index 000000000..af814a9ea
--- /dev/null
+++ b/src/main/res/layout/day_specific_activity_item.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 9c02608aa..2b5628deb 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -555,6 +555,11 @@ limitations under the License.
%1$s°
%1$s, %2$s
+
+ Calendar
+ Day Specific Run/Lift
+ Run
+
%1$d ft (excellent GPS)
%1$d ft (recommended)