mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Refactor workout details
This commit is contained in:
@@ -657,7 +657,7 @@
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.ActivitySummaryDetail"
|
||||
android:name=".activities.workouts.WorkoutDetailsActivity"
|
||||
android:label="@string/activity_summary_detail"
|
||||
android:parentActivityName=".activities.ActivitySummariesActivity"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
+50
-68
@@ -24,15 +24,12 @@ import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
@@ -57,6 +54,7 @@ import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutDetailsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
@@ -90,38 +88,30 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (Objects.requireNonNull(action)) {
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
LOG.error("Got device changed without device");
|
||||
return;
|
||||
}
|
||||
if (!device.equals(mGBDevice)) {
|
||||
return;
|
||||
}
|
||||
if (device.isBusy()) {
|
||||
swipeLayout.setRefreshing(true);
|
||||
} else {
|
||||
boolean wasBusy = swipeLayout.isRefreshing();
|
||||
swipeLayout.setRefreshing(false);
|
||||
if (wasBusy) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
break;
|
||||
if (!GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
|
||||
LOG.warn("Got unexpected action {}", intent.getAction());
|
||||
return;
|
||||
}
|
||||
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
LOG.error("Got device changed without device");
|
||||
return;
|
||||
}
|
||||
if (!device.equals(mGBDevice)) {
|
||||
return;
|
||||
}
|
||||
if (device.isBusy()) {
|
||||
swipeLayout.setRefreshing(true);
|
||||
} else {
|
||||
boolean wasBusy = swipeLayout.isRefreshing();
|
||||
swipeLayout.setRefreshing(false);
|
||||
if (wasBusy) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static int getBackgroundColor(Context context) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
theme.resolveAttribute(R.attr.sports_activity_summary_background, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
@@ -152,13 +142,14 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData);
|
||||
if (requestCode == ACTIVITY_FILTER && resultData != null) {
|
||||
if (requestCode == ACTIVITY_FILTER && resultData != null && resultData.getExtras() != null) {
|
||||
Bundle bundle = resultData.getExtras();
|
||||
activityFilter = bundle.getInt("activityFilter", 0);
|
||||
dateFromFilter = bundle.getLong("dateFromFilter", 0);
|
||||
dateToFilter = bundle.getLong("dateToFilter", 0);
|
||||
deviceFilter = bundle.getLong("deviceFilter", 0);
|
||||
nameContainsFilter = bundle.getString("nameContainsFilter");
|
||||
//noinspection unchecked
|
||||
itemsFilter = (List<Long>) bundle.getSerializable("itemsFilter");
|
||||
setActivityKindFilter(activityFilter);
|
||||
setDateFromFilter(dateFromFilter);
|
||||
@@ -206,7 +197,7 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
ActivitySummary summary = activitySummariesAdapter.getItem(position);
|
||||
if (summary != null) {
|
||||
try {
|
||||
showActivityDetail(position);
|
||||
showActivityDetail(position - 1);
|
||||
} catch (Exception e) {
|
||||
GB.toast(getApplicationContext(), "Unable to display Activity Detail, maybe the activity is not available yet: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
@@ -284,11 +275,7 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
.setTitle(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_title, toDelete.size()))
|
||||
.setMessage(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_description, toDelete.size()))
|
||||
.setIcon(R.drawable.ic_delete_forever)
|
||||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int whichButton) {
|
||||
deleteItems(toDelete);
|
||||
}
|
||||
})
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> deleteItems(toDelete))
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
|
||||
@@ -370,18 +357,15 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
|
||||
public void resetFetchTimestampToChosenDate() {
|
||||
final Calendar currentDate = Calendar.getInstance();
|
||||
new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() {
|
||||
@Override
|
||||
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
|
||||
Calendar date = Calendar.getInstance();
|
||||
date.set(year, monthOfYear, dayOfMonth);
|
||||
new DatePickerDialog(this, (view, year, monthOfYear, dayOfMonth) -> {
|
||||
Calendar date = Calendar.getInstance();
|
||||
date.set(year, monthOfYear, dayOfMonth);
|
||||
|
||||
long timestamp = date.getTimeInMillis() - 1000;
|
||||
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
|
||||
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
|
||||
editor.putLong("lastSportsActivityTimeMillis", timestamp);
|
||||
editor.apply();
|
||||
}
|
||||
long timestamp = date.getTimeInMillis() - 1000;
|
||||
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
|
||||
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
|
||||
editor.putLong("lastSportsActivityTimeMillis", timestamp);
|
||||
editor.apply();
|
||||
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
|
||||
}
|
||||
|
||||
@@ -433,23 +417,20 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void showActivityDetail(int position) {
|
||||
Intent ActivitySummaryDetailIntent = new Intent(this, ActivitySummaryDetail.class);
|
||||
Bundle bundle = new Bundle();
|
||||
private void showActivityDetail(final int position) {
|
||||
final Intent intent = WorkoutDetailsActivity.Companion.createFilteredListIntent(
|
||||
this,
|
||||
mGBDevice,
|
||||
position,
|
||||
activityFilter,
|
||||
dateFromFilter,
|
||||
dateToFilter,
|
||||
nameContainsFilter,
|
||||
deviceFilter,
|
||||
itemsFilter
|
||||
);
|
||||
|
||||
bundle.putInt("position", position);
|
||||
bundle.putSerializable("activityKindMap", activityKindMap);
|
||||
bundle.putSerializable("itemsFilter", (Serializable) itemsFilter);
|
||||
bundle.putInt("activityFilter", activityFilter);
|
||||
bundle.putLong("dateFromFilter", dateFromFilter);
|
||||
bundle.putLong("dateToFilter", dateToFilter);
|
||||
bundle.putLong("deviceFilter", deviceFilter);
|
||||
bundle.putLong("initial_deviceFilter", getDeviceId(mGBDevice));
|
||||
bundle.putString("nameContainsFilter", nameContainsFilter);
|
||||
ActivitySummaryDetailIntent.putExtras(bundle);
|
||||
|
||||
ActivitySummaryDetailIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
||||
startActivityForResult(ActivitySummaryDetailIntent, ACTIVITY_DETAIL);
|
||||
startActivityForResult(intent, ACTIVITY_DETAIL);
|
||||
}
|
||||
|
||||
private void runFilterActivity() {
|
||||
@@ -467,11 +448,12 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
startActivityForResult(filterIntent, ACTIVITY_FILTER);
|
||||
}
|
||||
|
||||
private long getDeviceId(GBDevice device) {
|
||||
private long getDeviceId(final GBDevice device) {
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
|
||||
return dbDevice.getId();
|
||||
final Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
|
||||
return Objects.requireNonNull(dbDevice).getId();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get device id", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+9
-1
@@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DefaultChartsData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SampleXLabelFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackViewModel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@@ -72,6 +73,8 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
private int startTime;
|
||||
private int endTime;
|
||||
|
||||
private boolean chartsSetUp;
|
||||
|
||||
@Override
|
||||
protected void onReceive(final Context context, final Intent intent) {
|
||||
// FIXME: We need to override this, or we crash
|
||||
@@ -90,6 +93,7 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
this.endTime = (int) endTime;
|
||||
this.gbDevice = gbDevice;
|
||||
if (this.view != null) {
|
||||
setupChart();
|
||||
createLocalRefreshTask("getting hr and activity", getActivity()).execute();
|
||||
}
|
||||
}
|
||||
@@ -124,6 +128,9 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
}
|
||||
|
||||
private void setupChart() {
|
||||
if (chartsSetUp) {
|
||||
return;
|
||||
}
|
||||
mChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(mChart);
|
||||
@@ -156,6 +163,7 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
yAxisRight.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
|
||||
yAxisRight.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
|
||||
|
||||
chartsSetUp = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,7 +227,7 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
final DefaultChartsData<LineData> activitySamplesData = buildChartFromSamples(handler);
|
||||
|
||||
if (trackFile != null) {
|
||||
final List<ActivityPoint> activityPoints = ActivitySummariesGpsFragment.getActivityPoints(trackFile)
|
||||
final List<ActivityPoint> activityPoints = MapsTrackViewModel.Companion.getActivityPoints(trackFile)
|
||||
.stream()
|
||||
.filter(ap -> ap.getHeartRate() > 0)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
/* Copyright (C) 2020-2024 José Rebelo, Petr Vaněk, a0z
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.mapsforge.core.model.BoundingBox;
|
||||
import org.mapsforge.core.model.Dimension;
|
||||
import org.mapsforge.core.model.LatLong;
|
||||
import org.mapsforge.core.model.MapPosition;
|
||||
import org.mapsforge.core.util.LatLongUtils;
|
||||
import org.mapsforge.map.android.view.MapView;
|
||||
import org.mapsforge.map.model.Model;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager;
|
||||
|
||||
|
||||
|
||||
public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class);
|
||||
|
||||
private MapView mapView;
|
||||
private File inputFile;
|
||||
private MapsManager mapsManager;
|
||||
|
||||
private boolean mapValid = true;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (MapsSettingsFragment.ACTION_SETTING_CHANGE.equals(intent.getAction())) {
|
||||
if (!isResumed()) {
|
||||
mapValid = false;
|
||||
} else {
|
||||
mapsManager.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_gps, container, false);
|
||||
TextView gpsWarning = rootView.findViewById(R.id.gpsWarning);
|
||||
mapView = rootView.findViewById(R.id.activitygpsview);
|
||||
mapView.setBuiltInZoomControls(false);
|
||||
|
||||
mapsManager = new MapsManager(requireContext(), mapView);
|
||||
mapsManager.loadMaps();
|
||||
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(MapsSettingsFragment.ACTION_SETTING_CHANGE);
|
||||
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(mReceiver, intentFilter);
|
||||
|
||||
if (mapsManager.isMapLoaded()) {
|
||||
gpsWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (inputFile != null) {
|
||||
processInBackgroundThread();
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!mapValid) {
|
||||
mapsManager.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(mReceiver);
|
||||
mapsManager.onDestroy();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void set_data(File inputFile) {
|
||||
this.inputFile = inputFile;
|
||||
if (mapView != null) { //first fragment inflate is AFTER this is called
|
||||
processInBackgroundThread();
|
||||
}
|
||||
}
|
||||
|
||||
private void processInBackgroundThread() {
|
||||
new Thread(() -> {
|
||||
final List<GPSCoordinate> points = getActivityPoints(inputFile)
|
||||
.stream()
|
||||
.map(ActivityPoint::getLocation)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
if (!points.isEmpty()) {
|
||||
drawTrack(mapView, (ArrayList<? extends GPSCoordinate>) points);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static List<ActivityPoint> getActivityPoints(final File trackFile) {
|
||||
final List<ActivityPoint> points = new ArrayList<>();
|
||||
if (trackFile == null) {
|
||||
return points;
|
||||
}
|
||||
if (trackFile.getName().endsWith(".gpx")) {
|
||||
try (FileInputStream inputStream = new FileInputStream(trackFile)) {
|
||||
final GpxParser gpxParser = new GpxParser(inputStream);
|
||||
points.addAll(gpxParser.getGpxFile().getActivityPoints());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to open {}", trackFile, e);
|
||||
} catch (final GpxParseException e) {
|
||||
LOG.error("Failed to parse gpx file", e);
|
||||
}
|
||||
} else if (trackFile.getName().endsWith(".fit")) {
|
||||
try {
|
||||
FitFile fitFile = FitFile.parseIncoming(trackFile);
|
||||
for (final RecordData record : fitFile.getRecords()) {
|
||||
if (record instanceof FitRecord) {
|
||||
points.add(((FitRecord) record).toActivityPoint());
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to open {}", trackFile, e);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse fit file", e);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Unknown file type {}", trackFile.getName());
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private void drawTrack(MapView mapView, final ArrayList<? extends GPSCoordinate> trackPoints) {
|
||||
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||
double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||
double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||
List<LatLong> latlongs = trackPoints.stream()
|
||||
.map(p -> new LatLong(p.getLatitude(), p.getLongitude()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
mapsManager.setTrack(latlongs);
|
||||
|
||||
final Model model = mapView.getModel();
|
||||
// FIXME: We need to offset the min latitude so the track gets centered - not sure why
|
||||
final BoundingBox boundingBox = new BoundingBox(minLat - (maxLat - minLat) / 4, minLon, maxLat, maxLon);
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int height = (int) (300 * displayMetrics.density);
|
||||
byte zoom = LatLongUtils.zoomForBounds(new Dimension(screenWidth, height), boundingBox, model.displayModel.getTileSize());
|
||||
model.mapViewPosition.setMapPosition(new MapPosition(boundingBox.getCenterPoint(), zoom));
|
||||
|
||||
final View.OnTouchListener controlTouchListener = new View.OnTouchListener() {
|
||||
private float startX, startY;
|
||||
private static final int TAP_THRESHOLD = 10;
|
||||
private static final int LONG_PRESS_TIME = 300;
|
||||
private long pressTime;
|
||||
private boolean isDrag = false;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
isDrag = false;
|
||||
startX = event.getX();
|
||||
startY = event.getY();
|
||||
pressTime = System.currentTimeMillis();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (Math.abs(startX - event.getX()) > TAP_THRESHOLD || Math.abs(startY - event.getY()) > TAP_THRESHOLD) {
|
||||
isDrag = true;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!isDrag && System.currentTimeMillis() - pressTime < LONG_PRESS_TIME) {
|
||||
openMapActivity();
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openMapActivity() {
|
||||
Intent intent = new Intent(getContext(), MapsTrackActivity.class);
|
||||
intent.putExtra("file", inputFile);
|
||||
startActivity(intent);
|
||||
}
|
||||
};
|
||||
mapView.setOnTouchListener(controlTouchListener);
|
||||
|
||||
mapView.invalidate();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected CharSequence getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
-743
@@ -1,743 +0,0 @@
|
||||
/* Copyright (C) 2020-2024 Andreas Shimokawa, Arjan Schrijver, Daniel Dakhno,
|
||||
José Rebelo, Petr Vaněk
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.gridlayout.widget.GridLayout;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
|
||||
|
||||
public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryDetail.class);
|
||||
BaseActivitySummary currentItem = null;
|
||||
private GBDevice gbDevice;
|
||||
private Menu mOptionsMenu;
|
||||
List<String> filesGpxList = new ArrayList<>();
|
||||
int selectedGpxIndex;
|
||||
String selectedGpxFile;
|
||||
File export_path_root = null; // original gpx file location, could still contain old gpx files
|
||||
File export_path_gpx = null; // new gpx file location
|
||||
|
||||
private ActivitySummariesChartFragment activitySummariesChartFragment;
|
||||
private ActivitySummariesGpsFragment activitySummariesGpsFragment;
|
||||
|
||||
private final WorkoutValueFormatter workoutValueFormatter = new WorkoutValueFormatter();
|
||||
|
||||
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 static Bitmap getScreenShot(View view, int height, int width, Context context) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
canvas.drawColor(GBApplication.getWindowBackgroundColor(context));
|
||||
view.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Context appContext = this.getApplicationContext();
|
||||
if (appContext instanceof GBApplication) {
|
||||
setContentView(R.layout.activity_summary_details);
|
||||
}
|
||||
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
Bundle bundle = intent.getExtras();
|
||||
gbDevice = bundle.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
final int position = bundle.getInt("position", 0);
|
||||
final int activityFilter = bundle.getInt("activityFilter", 0);
|
||||
final long dateFromFilter = bundle.getLong("dateFromFilter", 0);
|
||||
final long dateToFilter = bundle.getLong("dateToFilter", 0);
|
||||
final long deviceFilter = bundle.getLong("deviceFilter", 0);
|
||||
final String nameContainsFilter = bundle.getString("nameContainsFilter");
|
||||
final List<Long> itemsFilter = (List<Long>) bundle.getSerializable("itemsFilter");
|
||||
|
||||
final ActivitySummaryItems items = new ActivitySummaryItems(this, gbDevice, activityFilter, dateFromFilter, dateToFilter, nameContainsFilter, deviceFilter, itemsFilter);
|
||||
final ScrollView layout = findViewById(R.id.activity_summary_detail_scroll_layout);
|
||||
//final LinearLayout layout = findViewById(R.id.activity_summary_detail_relative_layout);
|
||||
|
||||
final Animation animFadeRight;
|
||||
final Animation animFadeLeft;
|
||||
final Animation animBounceLeft;
|
||||
final Animation animBounceRight;
|
||||
|
||||
animFadeRight = AnimationUtils.loadAnimation(
|
||||
this,
|
||||
R.anim.flyright);
|
||||
animFadeLeft = AnimationUtils.loadAnimation(
|
||||
this,
|
||||
R.anim.flyleft);
|
||||
animBounceLeft = AnimationUtils.loadAnimation(
|
||||
this,
|
||||
R.anim.bounceleft);
|
||||
animBounceRight = AnimationUtils.loadAnimation(
|
||||
this,
|
||||
R.anim.bounceright);
|
||||
|
||||
activitySummariesChartFragment = new ActivitySummariesChartFragment();
|
||||
activitySummariesGpsFragment = new ActivitySummariesGpsFragment();
|
||||
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.chartsFragmentHolder, activitySummariesChartFragment)
|
||||
.replace(R.id.gpsFragmentHolder, activitySummariesGpsFragment)
|
||||
.commit();
|
||||
|
||||
layout.setOnTouchListener(new SwipeEvents(this) {
|
||||
@Override
|
||||
public void onSwipeRight() {
|
||||
BaseActivitySummary newItem = items.getNextItem();
|
||||
if (newItem != null) {
|
||||
currentItem = newItem;
|
||||
refreshFromCurrentItem();
|
||||
layout.startAnimation(animFadeRight);
|
||||
} else {
|
||||
layout.startAnimation(animBounceRight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwipeLeft() {
|
||||
BaseActivitySummary newItem = items.getPrevItem();
|
||||
if (newItem != null) {
|
||||
currentItem = newItem;
|
||||
refreshFromCurrentItem();
|
||||
layout.startAnimation(animFadeLeft);
|
||||
} else {
|
||||
layout.startAnimation(animBounceLeft);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
currentItem = items.getItem(position);
|
||||
if (currentItem != null) {
|
||||
refreshFromCurrentItem();
|
||||
}
|
||||
|
||||
//allows long-press.switch of data being in raw form or recalculated
|
||||
ImageView activity_icon = findViewById(R.id.item_image);
|
||||
activity_icon.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
public boolean onLongClick(View v) {
|
||||
workoutValueFormatter.toggleRawData();
|
||||
if (currentItem != null) {
|
||||
makeSummaryHeader(currentItem);
|
||||
makeSummaryContent(currentItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void makeSummaryHeader(BaseActivitySummary item) {
|
||||
//make view of data from main part of item
|
||||
String activitykindname = ActivityKind.fromCode(item.getActivityKind()).getLabel(getApplicationContext());
|
||||
String activityname = item.getName();
|
||||
Date starttime = item.getStartTime();
|
||||
Date endtime = item.getEndTime();
|
||||
String durationhms = DateTimeUtils.formatDurationHoursMinutes((endtime.getTime() - starttime.getTime()), TimeUnit.MILLISECONDS);
|
||||
|
||||
ImageView activity_icon = findViewById(R.id.item_image);
|
||||
activity_icon.setImageResource(ActivityKind.fromCode(item.getActivityKind()).getIcon());
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(activitykindname);
|
||||
}
|
||||
|
||||
TextView activity_name = findViewById(R.id.activityname);
|
||||
activity_name.setText(activityname);
|
||||
if (StringUtils.isBlank(activityname)) {
|
||||
activity_name.setVisibility(View.GONE);
|
||||
} else {
|
||||
activity_name.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
TextView activityDateTextView = findViewById(R.id.activitydate);
|
||||
|
||||
final Context context = activityDateTextView.getContext();
|
||||
final String timeString;
|
||||
if (DateTimeUtils.isSameDay(starttime, endtime)) {
|
||||
timeString = context.getString(
|
||||
R.string.date_placeholders__start_time__end_time,
|
||||
DateTimeUtils.formatDateTimeRelative(context, starttime),
|
||||
DateTimeUtils.formatTime(endtime.getHours(), endtime.getMinutes())
|
||||
);
|
||||
} else {
|
||||
timeString = context.getString(
|
||||
R.string.date_placeholders__start_time__end_time,
|
||||
DateTimeUtils.formatDateTimeRelative(context, starttime),
|
||||
DateTimeUtils.formatDateTimeRelative(context, endtime)
|
||||
);
|
||||
}
|
||||
activityDateTextView.setText(timeString);
|
||||
|
||||
TextView activityTimeTextView = findViewById(R.id.activityduration);
|
||||
activityTimeTextView.setText(durationhms);
|
||||
}
|
||||
|
||||
private void refreshFromCurrentItem() {
|
||||
makeSummaryHeader(currentItem);
|
||||
makeSummaryContent(currentItem);
|
||||
|
||||
activitySummariesChartFragment.setDateAndGetData(
|
||||
getTrackFile(),
|
||||
getGBDevice(currentItem.getDevice()),
|
||||
currentItem.getStartTime().getTime() / 1000,
|
||||
currentItem.getEndTime().getTime() / 1000
|
||||
);
|
||||
|
||||
if (itemHasGps()) {
|
||||
showGpsCanvas();
|
||||
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||
} else {
|
||||
hideGpsCanvas();
|
||||
}
|
||||
|
||||
updateMenuItems();
|
||||
}
|
||||
|
||||
private File get_path() {
|
||||
File path = null;
|
||||
try {
|
||||
path = FileUtils.getExternalFilesDir();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error getting path", e);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private List<String> get_gpx_file_list() {
|
||||
List<File> files = new ArrayList<>();
|
||||
FileFilter gpxFileFilter = file -> file.isFile() && file.getPath().toLowerCase().endsWith(".gpx");
|
||||
if (export_path_root.isDirectory()) {
|
||||
File[] rootFiles = export_path_root.listFiles(gpxFileFilter);
|
||||
if (rootFiles != null)
|
||||
Collections.addAll(files, rootFiles);
|
||||
}
|
||||
if (export_path_gpx.isDirectory()) {
|
||||
File[] gpxFiles = export_path_gpx.listFiles(gpxFileFilter);
|
||||
if (gpxFiles != null)
|
||||
Collections.addAll(files, gpxFiles);
|
||||
}
|
||||
|
||||
files.sort((file1, file2) -> {
|
||||
long lastModified1 = file1.lastModified();
|
||||
long lastModified2 = file2.lastModified();
|
||||
return Long.compare(lastModified2, lastModified1); // Descending order
|
||||
});
|
||||
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add(getString(R.string.activity_summary_detail_clear_gpx_track));
|
||||
for (File file : files) {
|
||||
list.add(file.getName());
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void makeSummaryContent(BaseActivitySummary item) {
|
||||
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(gbDevice, this);
|
||||
|
||||
//make view of data from summaryData of item
|
||||
LinearLayout fieldLayout = findViewById(R.id.summaryDetails);
|
||||
fieldLayout.removeAllViews(); //remove old widgets
|
||||
ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, item);
|
||||
Map<String, List<Pair<String, ActivitySummaryEntry>>> data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
|
||||
if (data == null) return;
|
||||
|
||||
for (final Map.Entry<String, List<Pair<String, ActivitySummaryEntry>>> group : data.entrySet()) {
|
||||
final String groupKey = group.getKey();
|
||||
final List<Pair<String, ActivitySummaryEntry>> entries = group.getValue();
|
||||
|
||||
// Do not add a header to the first ACTIVITY group, it's redundant
|
||||
if (!ActivitySummaryEntries.GROUP_ACTIVITY.equals(groupKey)) {
|
||||
TableRow label_row = new TableRow(ActivitySummaryDetail.this);
|
||||
TextView label_field = new TextView(ActivitySummaryDetail.this);
|
||||
label_field.setId(View.generateViewId());
|
||||
label_field.setTextSize(18);
|
||||
label_field.setGravity(Gravity.CENTER);
|
||||
label_field.setPaddingRelative(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16));
|
||||
label_field.setTypeface(null, Typeface.BOLD);
|
||||
label_field.setText(workoutValueFormatter.getStringResourceByName(groupKey));
|
||||
label_row.addView(label_field);
|
||||
fieldLayout.addView(label_row);
|
||||
}
|
||||
|
||||
GridLayout gridLayout = new GridLayout(ActivitySummaryDetail.this);
|
||||
gridLayout.setBackgroundColor(getResources().getColor(R.color.gauge_line_color));
|
||||
gridLayout.setColumnCount(2);
|
||||
gridLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
int totalCells = entries.stream().mapToInt(e -> e.getRight().getColumnSpan()).sum();
|
||||
totalCells += totalCells & 1; // round up to nearest even number, since we have 2 columns
|
||||
int cellNumber = 0;
|
||||
for (final Pair<String, ActivitySummaryEntry> entry : entries) {
|
||||
final int columnSpan = entry.getRight().getColumnSpan();
|
||||
if (columnSpan == 2 && cellNumber % 2 != 0) {
|
||||
// This entry needs 2 columns, so let's move to the next row
|
||||
cellNumber++;
|
||||
}
|
||||
LinearLayout linearLayout = generateLinearLayout(cellNumber, cellNumber + 2 >= totalCells, columnSpan);
|
||||
entry.getRight().populate(entry.getLeft(), linearLayout, workoutValueFormatter);
|
||||
gridLayout.addView(linearLayout);
|
||||
cellNumber += columnSpan;
|
||||
}
|
||||
if (gridLayout.getChildCount() > 0) {
|
||||
if (cellNumber % 2 != 0) {
|
||||
final LinearLayout emptyLayout = generateLinearLayout(cellNumber, true, 1);
|
||||
new ActivitySummarySimpleEntry(null, "", "string").populate("", emptyLayout, workoutValueFormatter);
|
||||
gridLayout.addView(emptyLayout);
|
||||
}
|
||||
fieldLayout.addView(gridLayout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LinearLayout generateLinearLayout(int i, boolean lastRow, int columnSize) {
|
||||
LinearLayout linearLayout = new LinearLayout(ActivitySummaryDetail.this);
|
||||
GridLayout.LayoutParams columnParams = new GridLayout.LayoutParams();
|
||||
columnParams.columnSpec = GridLayout.spec(i % 2 == 0 ? 0 : 1, 1);
|
||||
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, columnSize, GridLayout.FILL, 1f)
|
||||
);
|
||||
layoutParams.width = 0;
|
||||
linearLayout.setLayoutParams(layoutParams);
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
linearLayout.setGravity(Gravity.CENTER);
|
||||
linearLayout.setPadding(dpToPx(15), dpToPx(15), dpToPx(15), dpToPx(15));
|
||||
linearLayout.setBackgroundColor(GBApplication.getWindowBackgroundColor(ActivitySummaryDetail.this));
|
||||
int marginLeft = 0;
|
||||
int marginTop = 2;
|
||||
int marginBottom = 0;
|
||||
int marginRight = 0;
|
||||
if (i % 2 == 0) {
|
||||
marginRight = 1;
|
||||
}
|
||||
if (i % 2 == 1) {
|
||||
marginLeft = 1;
|
||||
}
|
||||
if (lastRow) {
|
||||
marginBottom = 2;
|
||||
}
|
||||
layoutParams.setMargins(dpToPx(marginLeft), dpToPx(marginTop), dpToPx(marginRight), dpToPx(marginBottom));
|
||||
|
||||
return linearLayout;
|
||||
}
|
||||
|
||||
public int dpToPx(int dp) {
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
// back button
|
||||
finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_take_screenshot) {
|
||||
take_share_screenshot(ActivitySummaryDetail.this);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_show_gpx) {
|
||||
viewGpxTrack(ActivitySummaryDetail.this);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_share_gpx) {
|
||||
shareGpxTrack(ActivitySummaryDetail.this);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_inspect_file) {
|
||||
final Intent inspectFileIntent = new Intent(ActivitySummaryDetail.this, FitViewerActivity.class);
|
||||
inspectFileIntent.putExtra(FitViewerActivity.EXTRA_PATH, currentItem.getRawDetailsPath());
|
||||
startActivity(inspectFileIntent);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_share_raw_summary) {
|
||||
shareRawSummary(ActivitySummaryDetail.this, currentItem);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_share_raw_details) {
|
||||
shareRawDetails(ActivitySummaryDetail.this, currentItem);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_share_json_details) {
|
||||
shareJsonDetails(ActivitySummaryDetail.this, currentItem);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_summary_detail_action_edit_name) {
|
||||
editLabel();
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_summary_detail_action_edit_gps) {
|
||||
editGps();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void editLabel() {
|
||||
final EditText input = new EditText(ActivitySummaryDetail.this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
String name = currentItem.getName();
|
||||
input.setText((name != null) ? name : "");
|
||||
FrameLayout container = new FrameLayout(ActivitySummaryDetail.this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
|
||||
new MaterialAlertDialogBuilder(ActivitySummaryDetail.this)
|
||||
.setView(container)
|
||||
.setCancelable(true)
|
||||
.setTitle(ActivitySummaryDetail.this.getString(R.string.activity_summary_edit_name_title))
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
String name1 = input.getText().toString();
|
||||
if (name1.isEmpty()) name1 = null;
|
||||
currentItem.setName(name1);
|
||||
currentItem.update();
|
||||
makeSummaryHeader(currentItem);
|
||||
makeSummaryContent(currentItem);
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
|
||||
// do nothing
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void editGps() {
|
||||
export_path_root = get_path();
|
||||
export_path_gpx = new File(get_path(), "gpx");
|
||||
filesGpxList = get_gpx_file_list();
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ActivitySummaryDetail.this);
|
||||
builder.setTitle(R.string.activity_summary_detail_select_gpx_track);
|
||||
ArrayAdapter<String> directory_listing = new ArrayAdapter<>(ActivitySummaryDetail.this, android.R.layout.simple_list_item_1, filesGpxList);
|
||||
builder.setSingleChoiceItems(directory_listing, 0, (dialog, which) -> {
|
||||
selectedGpxIndex = which;
|
||||
String selectedFilename = filesGpxList.get(selectedGpxIndex);
|
||||
if (new File(export_path_gpx, selectedFilename).isFile()) {
|
||||
// Note: if selectedFilename exists in both export_path_gpx and export_path_root,
|
||||
// this code will always choose the one in export_path_gpx. This is acceptable
|
||||
// because export_path_gpx is where all new files end up, and gpx files tend to
|
||||
// have a unique name anyway because it usually contains some timestamp.
|
||||
selectedGpxFile = new File(export_path_gpx, selectedFilename).getPath();
|
||||
} else {
|
||||
selectedGpxFile = new File(export_path_root, selectedFilename).getPath();
|
||||
}
|
||||
String message = String.format("%s %s?", getString(R.string.set), filesGpxList.get(selectedGpxIndex));
|
||||
if (selectedGpxIndex == 0) {
|
||||
selectedGpxFile = null;
|
||||
message = String.format("%s?", getString(R.string.activity_summary_detail_clear_gpx_track));
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(ActivitySummaryDetail.this)
|
||||
.setCancelable(true)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setTitle(R.string.activity_summary_detail_editing_gpx_track)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
|
||||
currentItem.setGpxTrack(selectedGpxFile);
|
||||
currentItem.update();
|
||||
if (itemHasGps()) {
|
||||
showGpsCanvas();
|
||||
activitySummariesGpsFragment.set_data(getTrackFile());
|
||||
} else {
|
||||
hideGpsCanvas();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, (dialog2, which2) -> {
|
||||
})
|
||||
.show();
|
||||
dialog.dismiss();
|
||||
});
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void take_share_screenshot(Context context) {
|
||||
final ScrollView layout = findViewById(R.id.activity_summary_detail_scroll_layout);
|
||||
final int width = layout.getChildAt(0).getWidth();
|
||||
final int height = layout.getChildAt(0).getHeight();
|
||||
final Bitmap screenShot = getScreenShot(layout, height, width, context);
|
||||
|
||||
final String fileName = FileUtils.makeValidFileName("Screenshot-" + ActivityKind.fromCode(currentItem.getActivityKind()).getLabel(context).toLowerCase() + "-" + DateTimeUtils.formatIso8601(currentItem.getStartTime()) + ".png");
|
||||
try {
|
||||
final File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
|
||||
final FileOutputStream fOut = new FileOutputStream(targetFile);
|
||||
screenShot.compress(Bitmap.CompressFormat.PNG, 85, fOut);
|
||||
fOut.flush();
|
||||
fOut.close();
|
||||
shareScreenshot(targetFile, context);
|
||||
GB.toast(getApplicationContext(), "Screenshot saved", Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error getting screenshot", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void shareScreenshot(File targetFile, Context context) {
|
||||
Uri contentUri = FileProvider.getUriForFile(context,
|
||||
context.getApplicationContext().getPackageName() + ".screenshot_provider", targetFile);
|
||||
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
sharingIntent.setType("image/*");
|
||||
String shareBody = "Sports Activity";
|
||||
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Sports Activity");
|
||||
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
|
||||
sharingIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(sharingIntent, "Share via"));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(context, R.string.activity_error_no_app_for_png, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void viewGpxTrack(Context context) {
|
||||
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 {
|
||||
AndroidUtils.viewFile(gpxFile.getPath(), "application/gpx+xml", context);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getApplicationContext(), "Unable to display GPX track: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void shareGpxTrack(final Context context) {
|
||||
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 {
|
||||
AndroidUtils.shareFile(context, gpxFile);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Unable to share GPX track: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawSummaryFilename = FileUtils.makeValidFileName(String.format("%s_summary.bin", DateTimeUtils.formatIso8601(summary.getStartTime())));
|
||||
|
||||
try {
|
||||
AndroidUtils.shareBytesAsFile(context, rawSummaryFilename, summary.getRawSummaryData());
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Unable to share GPX track: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void shareRawDetails(final Context context, final BaseActivitySummary summary) {
|
||||
if (summary.getRawDetailsPath() == null) {
|
||||
GB.toast(context, "No raw details in this activity", Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AndroidUtils.shareFile(context, new File(summary.getRawDetailsPath()));
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Unable to share raw details: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void shareJsonDetails(final Context context, final BaseActivitySummary summary) {
|
||||
if (summary.getSummaryData() == null) {
|
||||
GB.toast(context, "No json details in this activity", Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
final String jsonSummaryFilename = FileUtils.makeValidFileName(String.format("%s.json", DateTimeUtils.formatIso8601(summary.getStartTime())));
|
||||
|
||||
try {
|
||||
AndroidUtils.shareBytesAsFile(context, jsonSummaryFilename, summary.getSummaryData().getBytes(StandardCharsets.UTF_8));
|
||||
} catch (final Exception e) {
|
||||
GB.toast(context, "Unable to share json details: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMenuItems() {
|
||||
boolean hasGpx = false;
|
||||
boolean hasRawSummary = false;
|
||||
boolean hasRawDetails = false;
|
||||
|
||||
if (currentItem != null) {
|
||||
hasGpx = itemHasGps();
|
||||
hasRawSummary = currentItem.getRawSummaryData() != null;
|
||||
|
||||
final String rawDetailsPath = currentItem.getRawDetailsPath();
|
||||
if (rawDetailsPath != null) {
|
||||
hasRawDetails = new File(rawDetailsPath).exists();
|
||||
}
|
||||
}
|
||||
|
||||
if (mOptionsMenu != null) {
|
||||
final SubMenu overflowMenu = mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu();
|
||||
if (overflowMenu != null) {
|
||||
overflowMenu.findItem(R.id.activity_action_show_gpx).setVisible(hasGpx);
|
||||
overflowMenu.findItem(R.id.activity_action_share_gpx).setVisible(hasGpx);
|
||||
overflowMenu.findItem(R.id.activity_action_dev_inspect_file).setVisible(hasRawDetails && currentItem.getRawDetailsPath().toLowerCase(Locale.ROOT).endsWith(".fit"));
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary);
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails);
|
||||
final MenuItem devToolsMenu = overflowMenu.findItem(R.id.activity_action_dev_tools);
|
||||
final SubMenu devToolsSubMenu = devToolsMenu.getSubMenu();
|
||||
devToolsMenu.setVisible(devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showGpsCanvas() {
|
||||
final View gpsView = findViewById(R.id.gpsFragmentHolder);
|
||||
final ViewGroup.LayoutParams params = gpsView.getLayoutParams();
|
||||
params.height = (int) (300 * getApplicationContext().getResources().getDisplayMetrics().density);
|
||||
gpsView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private void hideGpsCanvas() {
|
||||
final View gpsView = findViewById(R.id.gpsFragmentHolder);
|
||||
final ViewGroup.LayoutParams params = gpsView.getLayoutParams();
|
||||
params.height = 0;
|
||||
gpsView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private boolean itemHasGps() {
|
||||
if (currentItem.getGpxTrack() != null) {
|
||||
final File existing = FileUtils.tryFixPath(new File(currentItem.getGpxTrack()));
|
||||
if (existing != null && existing.canRead()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
final String summaryDataJson = currentItem.getSummaryData();
|
||||
if (summaryDataJson != null && summaryDataJson.contains(INTERNAL_HAS_GPS)) {
|
||||
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summaryDataJson);
|
||||
return summaryData != null && summaryData.getBoolean(INTERNAL_HAS_GPS, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private File getTrackFile() {
|
||||
return ActivitySummaryUtils.getTrackFile(currentItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
mOptionsMenu = menu;
|
||||
getMenuInflater().inflate(R.menu.activity_take_screenshot_menu, menu);
|
||||
updateMenuItems();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GBDevice getGBDevice(final Device findDevice) {
|
||||
return GBApplication.app().getDeviceManager().getDevices()
|
||||
.stream()
|
||||
.filter(d -> d.getAddress().equalsIgnoreCase(findDevice.getIdentifier()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
+4
-36
@@ -14,9 +14,7 @@ import android.widget.Toast
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
|
||||
@@ -29,10 +27,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager
|
||||
import org.mapsforge.core.model.BoundingBox
|
||||
import org.mapsforge.core.model.Dimension
|
||||
import org.mapsforge.core.model.LatLong
|
||||
import org.mapsforge.core.util.LatLongUtils
|
||||
|
||||
class GpxRouteInstallerActivity : AbstractGBActivity(), InstallActivity {
|
||||
companion object {
|
||||
@@ -262,7 +256,7 @@ class GpxRouteInstallerActivity : AbstractGBActivity(), InstallActivity {
|
||||
}
|
||||
|
||||
setInstallEnabled(false)
|
||||
installHandler?.onStartInstall(device)
|
||||
installHandler.onStartInstall(device)
|
||||
|
||||
val bundle = Bundle().apply {
|
||||
putString(GpxRouteInstallHandler.EXTRA_TRACK_NAME, trackName)
|
||||
@@ -285,36 +279,10 @@ class GpxRouteInstallerActivity : AbstractGBActivity(), InstallActivity {
|
||||
|
||||
private fun loadMapTrack(gpxFile: GpxFile) {
|
||||
lifecycleScope.launch {
|
||||
val (center, zoom, trackPoints) = withContext(Dispatchers.Default) {
|
||||
val points = gpxFile.activityPoints.mapNotNull { it.location }
|
||||
val points = gpxFile.activityPoints.mapNotNull { it.location }
|
||||
|
||||
if (points.isEmpty()) {
|
||||
return@withContext Triple(null, null, emptyList<LatLong>())
|
||||
}
|
||||
|
||||
val latitudes = points.map { it.latitude }
|
||||
val longitudes = points.map { it.longitude }
|
||||
|
||||
val maxLat = latitudes.maxOrNull() ?: 0.0
|
||||
val minLat = latitudes.minOrNull() ?: 0.0
|
||||
val maxLon = longitudes.maxOrNull() ?: 0.0
|
||||
val minLon = longitudes.minOrNull() ?: 0.0
|
||||
|
||||
val latLongs = points.map { LatLong(it.latitude, it.longitude) }
|
||||
|
||||
val mapCenter = LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2)
|
||||
val mapZoom = LatLongUtils.zoomForBounds(
|
||||
Dimension(resources.displayMetrics.widthPixels, resources.displayMetrics.heightPixels),
|
||||
BoundingBox(minLat, minLon, maxLat, maxLon),
|
||||
binding.gpxMapView.model.displayModel.tileSize
|
||||
)
|
||||
Triple(mapCenter, mapZoom, latLongs)
|
||||
}
|
||||
|
||||
if (trackPoints.isNotEmpty() && center != null && zoom != null) {
|
||||
mapsManager?.setTrack(trackPoints)
|
||||
binding.gpxMapView.setCenter(center)
|
||||
binding.gpxMapView.setZoomLevel(zoom)
|
||||
if (points.isNotEmpty()) {
|
||||
mapsManager?.setTrack(points)
|
||||
} else {
|
||||
binding.gpxMapView.visibility = View.GONE
|
||||
}
|
||||
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
/* Copyright (C) 2024 José Rebelo, a0z
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.maps;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.mapsforge.core.model.BoundingBox;
|
||||
import org.mapsforge.core.model.Dimension;
|
||||
import org.mapsforge.core.model.LatLong;
|
||||
import org.mapsforge.core.util.LatLongUtils;
|
||||
import org.mapsforge.map.android.view.MapView;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesGpsFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager;
|
||||
|
||||
public class MapsTrackActivity extends AbstractGBActivity implements MenuProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MapsTrackActivity.class);
|
||||
|
||||
private MapView mapView;
|
||||
private File file;
|
||||
public static boolean isActivityOpen = false;
|
||||
|
||||
private MapsManager mapsManager;
|
||||
|
||||
private boolean mapValid = true;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (MapsSettingsFragment.ACTION_SETTING_CHANGE.equals(intent.getAction())) {
|
||||
mapValid = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_maps_track);
|
||||
|
||||
if (isActivityOpen) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
isActivityOpen = true;
|
||||
|
||||
addMenuProvider(this);
|
||||
|
||||
mapView = findViewById(R.id.activitygpsview);
|
||||
mapsManager = new MapsManager(this, mapView);
|
||||
mapsManager.loadMaps();
|
||||
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(MapsSettingsFragment.ACTION_SETTING_CHANGE);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, intentFilter);
|
||||
|
||||
file = (File) getIntent().getExtras().get("file");
|
||||
final List<GPSCoordinate> trackPoints = ActivitySummariesGpsFragment.getActivityPoints(file)
|
||||
.stream()
|
||||
.map(ActivityPoint::getLocation)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
if (trackPoints.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||
double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||
double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||
List<LatLong> latlongs = trackPoints.stream()
|
||||
.map(p -> new LatLong(p.getLatitude(), p.getLongitude()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
mapsManager.setTrack(latlongs);
|
||||
|
||||
mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2));
|
||||
byte zoom = LatLongUtils.zoomForBounds(new Dimension(this.getResources().getDisplayMetrics().widthPixels, this.getResources().getDisplayMetrics().heightPixels), new BoundingBox(minLat, minLon, maxLat, maxLon), mapView.getModel().displayModel.getTileSize());
|
||||
mapView.setZoomLevel(zoom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
// back button
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle state) {
|
||||
super.onSaveInstanceState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
synchronized (MapsTrackActivity.class) {
|
||||
isActivityOpen = false;
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
|
||||
mapsManager.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (!mapValid) {
|
||||
mapsManager.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.maps_track_menu, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem menuItem) {
|
||||
final int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.maps_settings) {
|
||||
final Intent enableIntent = new Intent(this, MapsSettingsActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/* Copyright (C) 2024-2025 José Rebelo, a0z
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.maps
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityMapsTrackBinding
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
class MapsTrackActivity : AbstractGBActivity(), MenuProvider {
|
||||
private lateinit var binding: ActivityMapsTrackBinding
|
||||
private val viewModel: MapsTrackViewModel by viewModels()
|
||||
|
||||
private lateinit var mapsManager: MapsManager
|
||||
private var mapValid = true
|
||||
|
||||
private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (MapsSettingsFragment.ACTION_SETTING_CHANGE == intent.action) {
|
||||
mapValid = false
|
||||
return
|
||||
}
|
||||
|
||||
LOG.warn("Unknown action: {}", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMapsTrackBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val file = intent.extras?.getSerializable("file") as? File
|
||||
if (file == null) {
|
||||
GB.toast(this, "No file provided", Toast.LENGTH_LONG, GB.ERROR)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
addMenuProvider(this)
|
||||
|
||||
mapsManager = MapsManager(this, binding.mapView)
|
||||
mapsManager.loadMaps()
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, IntentFilter().apply {
|
||||
addAction(MapsSettingsFragment.ACTION_SETTING_CHANGE)
|
||||
})
|
||||
|
||||
viewModel.loadTrackData(file)
|
||||
|
||||
observeViewModel()
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.trackPoints.observe(this) { points ->
|
||||
if (points.isNotEmpty()) {
|
||||
mapsManager.setTrack(points)
|
||||
} else {
|
||||
LOG.warn("No track points to display or file was empty.")
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) { isLoading ->
|
||||
// TODO loading spinner?
|
||||
}
|
||||
|
||||
viewModel.error.observe(this) { error ->
|
||||
error?.let {
|
||||
GB.toast(this, it.localizedMessage, Toast.LENGTH_LONG, GB.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver)
|
||||
mapsManager.onDestroy()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!mapValid) {
|
||||
mapsManager.reload()
|
||||
mapValid = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.maps_track_menu, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.maps_settings -> {
|
||||
val enableIntent = Intent(this, MapsSettingsActivity::class.java)
|
||||
startActivity(enableIntent)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(MapsTrackActivity::class.java)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.maps
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
class MapsTrackViewModel : ViewModel() {
|
||||
private val _trackPoints = MutableLiveData<List<GPSCoordinate>>()
|
||||
val trackPoints: LiveData<List<GPSCoordinate>> = _trackPoints
|
||||
|
||||
private val _isLoading = MutableLiveData<Boolean>()
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
|
||||
private val _error = MutableLiveData<Exception?>()
|
||||
val error: LiveData<Exception?> = _error
|
||||
|
||||
fun loadTrackData(trackFile: File) {
|
||||
_isLoading.value = true
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val points = fetchTrackPoints(trackFile)
|
||||
if (points.isNotEmpty()) {
|
||||
_trackPoints.postValue(points)
|
||||
} else {
|
||||
LOG.warn("No track points found in file: ${trackFile.name}")
|
||||
_trackPoints.postValue(emptyList())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error loading track points", e)
|
||||
_error.postValue(e)
|
||||
} finally {
|
||||
_isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchTrackPoints(trackFile: File): List<GPSCoordinate> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
getActivityPoints(trackFile).mapNotNull { it.location }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(MapsTrackViewModel::class.java)
|
||||
|
||||
fun getActivityPoints(trackFile: File): List<ActivityPoint> {
|
||||
try {
|
||||
when {
|
||||
trackFile.name.endsWith(".gpx") -> {
|
||||
FileInputStream(trackFile).use { inputStream ->
|
||||
val gpxParser = GpxParser(inputStream)
|
||||
return gpxParser.gpxFile.activityPoints
|
||||
}
|
||||
}
|
||||
|
||||
trackFile.name.endsWith(".fit") -> {
|
||||
val fitFile = FitFile.parseIncoming(trackFile)
|
||||
return fitFile.records
|
||||
.filterIsInstance<FitRecord>()
|
||||
.map { it.toActivityPoint() }
|
||||
}
|
||||
|
||||
else -> {
|
||||
LOG.warn("Unknown file type: ${trackFile.name}")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
LOG.error("Failed to open $trackFile", e)
|
||||
} catch (e: GpxParseException) {
|
||||
LOG.error("Failed to parse gpx file", e)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to parse fit file", e)
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.coroutines.launch
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityWorkoutDetailsBinding
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind
|
||||
|
||||
class WorkoutDetailsActivity : AbstractGBActivity() {
|
||||
private val viewModel: WorkoutDetailsViewModel by viewModels()
|
||||
private lateinit var binding: ActivityWorkoutDetailsBinding
|
||||
private lateinit var pagerAdapter: WorkoutViewPagerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityWorkoutDetailsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setupViewPager()
|
||||
setupViewModel()
|
||||
}
|
||||
|
||||
private fun setupViewPager() {
|
||||
pagerAdapter = WorkoutViewPagerAdapter(this)
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
binding.viewPager.offscreenPageLimit = 1
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
viewModel.setCurrentPosition(position)
|
||||
|
||||
val summary = viewModel.workouts.value?.get(position)
|
||||
summary?.activityKind?.let {
|
||||
val activityKindName = ActivityKind.fromCode(it).getLabel(this@WorkoutDetailsActivity)
|
||||
// Action bar title
|
||||
supportActionBar?.title = activityKindName
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupViewModel() {
|
||||
val bundle = intent.extras ?: return
|
||||
|
||||
val gbDevice = bundle.getParcelable<GBDevice>(GBDevice.EXTRA_DEVICE)
|
||||
val singleWorkoutId = bundle.getLong(EXTRA_WORKOUT_ID, -1L)
|
||||
|
||||
if (singleWorkoutId != -1L) {
|
||||
// Single workout
|
||||
lifecycleScope.launch {
|
||||
viewModel.loadSingleWorkout(singleWorkoutId)
|
||||
}
|
||||
} else {
|
||||
// Filtered list
|
||||
val position = bundle.getInt("position", 0)
|
||||
val activityFilter = bundle.getInt("activityFilter", 0)
|
||||
val dateFromFilter = bundle.getLong("dateFromFilter", 0)
|
||||
val dateToFilter = bundle.getLong("dateToFilter", 0)
|
||||
val deviceFilter = bundle.getLong("deviceFilter", 0)
|
||||
val nameContainsFilter = bundle.getString("nameContainsFilter")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val itemsFilter = bundle.getSerializable("itemsFilter") as? List<Long>
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.loadFilteredWorkouts(
|
||||
gbDevice,
|
||||
activityFilter,
|
||||
dateFromFilter,
|
||||
dateToFilter,
|
||||
nameContainsFilter,
|
||||
deviceFilter,
|
||||
itemsFilter,
|
||||
position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.workouts.observe(this) { workouts ->
|
||||
pagerAdapter.updateWorkouts(workouts)
|
||||
}
|
||||
|
||||
viewModel.currentPosition.observe(this) { position ->
|
||||
if (binding.viewPager.currentItem != position) {
|
||||
binding.viewPager.setCurrentItem(position, false)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) { isLoading ->
|
||||
showLoading(isLoading)
|
||||
}
|
||||
|
||||
viewModel.error.observe(this) { error ->
|
||||
error?.let { showError(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoading(isLoading: Boolean) {
|
||||
binding.loadingSpinner.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.viewPager.visibility = if (isLoading) View.GONE else View.VISIBLE
|
||||
binding.errorMessage.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
binding.loadingSpinner.visibility = View.GONE
|
||||
binding.viewPager.visibility = View.GONE
|
||||
binding.errorMessage.visibility = View.VISIBLE
|
||||
binding.errorMessage.text = message
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_WORKOUT_ID = "workout_id"
|
||||
|
||||
fun createSingleWorkoutIntent(
|
||||
context: Context,
|
||||
workoutId: Long,
|
||||
gbDevice: GBDevice
|
||||
): Intent {
|
||||
return Intent(context, WorkoutDetailsActivity::class.java).apply {
|
||||
putExtra(EXTRA_WORKOUT_ID, workoutId)
|
||||
putExtra(GBDevice.EXTRA_DEVICE, gbDevice)
|
||||
}
|
||||
}
|
||||
|
||||
fun createFilteredListIntent(
|
||||
context: Context,
|
||||
gbDevice: GBDevice,
|
||||
position: Int = 0,
|
||||
activityFilter: Int = 0,
|
||||
dateFromFilter: Long = 0,
|
||||
dateToFilter: Long = 0,
|
||||
nameContainsFilter: String? = null,
|
||||
deviceFilter: Long = 0,
|
||||
itemsFilter: List<Long>? = null
|
||||
): Intent {
|
||||
return Intent(context, WorkoutDetailsActivity::class.java).apply {
|
||||
putExtra(GBDevice.EXTRA_DEVICE, gbDevice)
|
||||
putExtra("position", position)
|
||||
putExtra("activityFilter", activityFilter)
|
||||
putExtra("dateFromFilter", dateFromFilter)
|
||||
putExtra("dateToFilter", dateToFilter)
|
||||
nameContainsFilter?.let { putExtra("nameContainsFilter", it) }
|
||||
putExtra("deviceFilter", deviceFilter)
|
||||
itemsFilter?.let { putExtra("itemsFilter", ArrayList(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+719
@@ -0,0 +1,719 @@
|
||||
/* Copyright (C) 2020-2025 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryGroup
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry
|
||||
import nodomain.freeyourgadget.gadgetbridge.databinding.FragmentWorkoutDetailsBinding
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
private var workoutId: Long = -1
|
||||
private var currentWorkout: Workout? = null
|
||||
private var gbDevice: GBDevice? = null
|
||||
|
||||
private lateinit var binding: FragmentWorkoutDetailsBinding
|
||||
|
||||
private var chartFragment: ActivitySummariesChartFragment? = null
|
||||
private var gpsFragment: WorkoutGpsFragment? = null
|
||||
|
||||
private lateinit var workoutEditor: WorkoutEditor
|
||||
|
||||
private val workoutValueFormatter = WorkoutValueFormatter()
|
||||
|
||||
private var menu: Menu? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
workoutEditor = WorkoutEditor(requireContext())
|
||||
arguments?.let {
|
||||
workoutId = it.getLong(ARG_WORKOUT_ID, -1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentWorkoutDetailsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
chartFragment = ActivitySummariesChartFragment()
|
||||
gpsFragment = WorkoutGpsFragment()
|
||||
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.chartsFragmentHolder, chartFragment!!)
|
||||
.replace(R.id.gpsFragmentHolder, gpsFragment!!)
|
||||
.commit()
|
||||
|
||||
// Toggle raw data when long pressing the workout image
|
||||
binding.itemImage.setOnLongClickListener {
|
||||
workoutValueFormatter.toggleRawData()
|
||||
currentWorkout?.let { workout ->
|
||||
updateWorkoutHeader(workout.summary)
|
||||
updateWorkoutDetails(workout)
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
loadWorkoutData()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
currentWorkout?.summary?.activityKind?.let {
|
||||
val activityKindName = ActivityKind.fromCode(it).getLabel(requireContext())
|
||||
// Action bar title
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = activityKindName
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWorkoutData() {
|
||||
if (workoutId == -1L) return
|
||||
|
||||
showLoading(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
currentWorkout = withContext(Dispatchers.IO) {
|
||||
val summary = GBApplication.acquireDB().use { dbHandler ->
|
||||
dbHandler.daoSession.baseActivitySummaryDao.load(workoutId)
|
||||
}
|
||||
gbDevice = getGBDevice(summary.device)
|
||||
return@withContext gbDevice!!.deviceCoordinator.getActivitySummaryParser(gbDevice, requireContext())
|
||||
.parseWorkout(summary, true)
|
||||
}
|
||||
|
||||
requireActivity().addMenuProvider(this@WorkoutDetailsFragment, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
currentWorkout?.let { workout ->
|
||||
updateWorkoutHeader(workout.summary)
|
||||
updateWorkoutDetails(workout)
|
||||
updateFragments(workout)
|
||||
showLoading(false)
|
||||
} ?: run {
|
||||
showError("Workout not found")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error loading workout data", e)
|
||||
showError("Failed to load workout: ${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoading(isLoading: Boolean) {
|
||||
binding.loadingSpinner.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.scrollView.visibility = if (isLoading) View.GONE else View.VISIBLE
|
||||
binding.errorMessage.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
binding.loadingSpinner.visibility = View.GONE
|
||||
binding.scrollView.visibility = View.GONE
|
||||
binding.errorMessage.visibility = View.VISIBLE
|
||||
binding.errorMessage.text = message
|
||||
}
|
||||
|
||||
private fun updateWorkoutHeader(summary: BaseActivitySummary) {
|
||||
val activityName = summary.name
|
||||
val startTime = summary.startTime
|
||||
val endTime = summary.endTime
|
||||
val durationHms = DateTimeUtils.formatDurationHoursMinutes(
|
||||
endTime.time - startTime.time, TimeUnit.MILLISECONDS
|
||||
)
|
||||
|
||||
view?.let { view ->
|
||||
binding.itemImage.setImageResource(
|
||||
ActivityKind.fromCode(summary.activityKind).icon
|
||||
)
|
||||
|
||||
// Activity name
|
||||
binding.activityname.apply {
|
||||
text = activityName
|
||||
visibility = if (StringUtils.isBlank(activityName)) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
// Date
|
||||
binding.activitydate.apply {
|
||||
val timeString = if (DateTimeUtils.isSameDay(startTime, endTime)) {
|
||||
context.getString(
|
||||
R.string.date_placeholders__start_time__end_time,
|
||||
DateTimeUtils.formatDateTimeRelative(context, startTime),
|
||||
DateTimeUtils.formatTime(endTime.hours, endTime.minutes)
|
||||
)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.date_placeholders__start_time__end_time,
|
||||
DateTimeUtils.formatDateTimeRelative(context, startTime),
|
||||
DateTimeUtils.formatDateTimeRelative(context, endTime)
|
||||
)
|
||||
}
|
||||
text = timeString
|
||||
}
|
||||
|
||||
// Duration
|
||||
binding.activityduration.text = durationHms
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWorkoutDetails(workout: Workout) {
|
||||
binding.summaryDetails.removeAllViews()
|
||||
|
||||
val groups = ActivitySummaryGroup.buildGroupedList(workout.data)
|
||||
|
||||
for ((groupKey, entries) in groups) {
|
||||
if (ActivitySummaryEntries.GROUP_ACTIVITY != groupKey) {
|
||||
addGroupHeader(groupKey)
|
||||
}
|
||||
addGroupContent(entries)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGroupHeader(groupKey: String) {
|
||||
val labelRow = TableRow(context)
|
||||
val labelField = TextView(context).apply {
|
||||
id = View.generateViewId()
|
||||
textSize = 18f
|
||||
gravity = Gravity.CENTER
|
||||
setPaddingRelative(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16))
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
text = workoutValueFormatter.getStringResourceByName(groupKey)
|
||||
}
|
||||
labelRow.addView(labelField)
|
||||
binding.summaryDetails.addView(labelRow)
|
||||
}
|
||||
|
||||
private fun addGroupContent(entries: List<Pair<String, ActivitySummaryEntry>>) {
|
||||
val gridLayout = GridLayout(context).apply {
|
||||
setBackgroundColor(resources.getColor(R.color.gauge_line_color))
|
||||
columnCount = 2
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
val totalCells = entries.sumOf { it.right.columnSpan }.let { it + (it and 1) }
|
||||
var cellNumber = 0
|
||||
|
||||
for ((key, entry) in entries) {
|
||||
val columnSpan = entry.columnSpan
|
||||
if (columnSpan == 2 && cellNumber % 2 != 0) {
|
||||
cellNumber++
|
||||
}
|
||||
|
||||
val linearLayout = generateLinearLayout(cellNumber, cellNumber + 2 >= totalCells, columnSpan)
|
||||
entry.populate(key, linearLayout, workoutValueFormatter)
|
||||
gridLayout.addView(linearLayout)
|
||||
cellNumber += columnSpan
|
||||
}
|
||||
|
||||
if (gridLayout.isNotEmpty()) {
|
||||
if (cellNumber % 2 != 0) {
|
||||
val emptyLayout = generateLinearLayout(cellNumber, true, 1)
|
||||
ActivitySummarySimpleEntry(null, "", "string").populate("", emptyLayout, workoutValueFormatter)
|
||||
gridLayout.addView(emptyLayout)
|
||||
}
|
||||
binding.summaryDetails.addView(gridLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateLinearLayout(i: Int, lastRow: Boolean, columnSize: Int): LinearLayout {
|
||||
return LinearLayout(context).apply {
|
||||
val layoutParams = GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, columnSize, GridLayout.FILL, 1f)
|
||||
)
|
||||
layoutParams.width = 0
|
||||
this.layoutParams = layoutParams
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(dpToPx(15), dpToPx(15), dpToPx(15), dpToPx(15))
|
||||
setBackgroundColor(GBApplication.getWindowBackgroundColor(context))
|
||||
|
||||
val marginLeft = if (i % 2 == 0) 0 else 1
|
||||
val marginRight = if (i % 2 == 0) 1 else 0
|
||||
val marginTop = 2
|
||||
val marginBottom = if (lastRow) 2 else 0
|
||||
|
||||
layoutParams.setMargins(dpToPx(marginLeft), dpToPx(marginTop), dpToPx(marginRight), dpToPx(marginBottom))
|
||||
}
|
||||
}
|
||||
|
||||
private fun dpToPx(dp: Int): Int {
|
||||
val density = resources.displayMetrics.density
|
||||
return (dp * density).toInt()
|
||||
}
|
||||
|
||||
private fun updateFragments(workout: Workout) {
|
||||
val trackFile = ActivitySummaryUtils.getTrackFile(workout.summary)
|
||||
|
||||
chartFragment?.setDateAndGetData(
|
||||
trackFile,
|
||||
gbDevice,
|
||||
workout.summary.startTime.time / 1000,
|
||||
workout.summary.endTime.time / 1000
|
||||
)
|
||||
|
||||
binding.dynamicCharts.removeAllViews()
|
||||
for (chart in workout.charts) {
|
||||
addChart(chart)
|
||||
}
|
||||
|
||||
if (workoutHasGps(workout.summary)) {
|
||||
showGpsCanvas()
|
||||
gpsFragment?.setTrackData(trackFile)
|
||||
} else {
|
||||
hideGpsCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChart(chart: WorkoutChart) {
|
||||
val separator1 = View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
(2 * resources.displayMetrics.density).toInt()
|
||||
)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(R.attr.row_separator, typedValue, true)
|
||||
setBackgroundColor(ContextCompat.getColor(context, typedValue.resourceId))
|
||||
}
|
||||
binding.dynamicCharts.addView(separator1)
|
||||
|
||||
val chartTitle = TextView(context).apply {
|
||||
id = View.generateViewId()
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
text = chart.title
|
||||
gravity = Gravity.CENTER
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
||||
typeface = Typeface.create("sans-serif-black", Typeface.NORMAL)
|
||||
|
||||
val paddingPx = (16 * resources.displayMetrics.density).toInt()
|
||||
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
|
||||
}
|
||||
binding.dynamicCharts.addView(chartTitle)
|
||||
|
||||
val separator2 = View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
(2 * resources.displayMetrics.density).toInt()
|
||||
)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(R.attr.row_separator, typedValue, true)
|
||||
setBackgroundColor(ContextCompat.getColor(context, typedValue.resourceId))
|
||||
}
|
||||
binding.dynamicCharts.addView(separator2)
|
||||
|
||||
val chartsFragmentHolder = FrameLayout(requireContext()).apply {
|
||||
id = View.generateViewId()
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
(300 * resources.displayMetrics.density).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO add chartData
|
||||
|
||||
binding.dynamicCharts.addView(chartsFragmentHolder)
|
||||
}
|
||||
|
||||
private fun workoutHasGps(summary: BaseActivitySummary): Boolean {
|
||||
summary.gpxTrack?.let { gpxTrack ->
|
||||
val existing = FileUtils.tryFixPath(File(gpxTrack))
|
||||
if (existing != null && existing.canRead()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val summaryDataJson = summary.summaryData
|
||||
if (summaryDataJson != null && summaryDataJson.contains(ActivitySummaryEntries.INTERNAL_HAS_GPS)) {
|
||||
val summaryData = ActivitySummaryData.fromJson(summaryDataJson)
|
||||
return summaryData.getBoolean(ActivitySummaryEntries.INTERNAL_HAS_GPS, false)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun showGpsCanvas() {
|
||||
val params = binding.gpsFragmentHolder.layoutParams
|
||||
params.height = (300 * requireContext().resources.displayMetrics.density).toInt()
|
||||
binding.gpsFragmentHolder.layoutParams = params
|
||||
}
|
||||
|
||||
private fun hideGpsCanvas() {
|
||||
val params = binding.gpsFragmentHolder.layoutParams
|
||||
params.height = 0
|
||||
binding.gpsFragmentHolder.layoutParams = params
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menu.clear()
|
||||
menuInflater.inflate(R.menu.activity_take_screenshot_menu, menu)
|
||||
this.menu = menu
|
||||
updateMenuItems(menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val workout = currentWorkout ?: return false
|
||||
|
||||
return when (menuItem.itemId) {
|
||||
R.id.activity_action_take_screenshot -> {
|
||||
takeSharedScreenshot()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_show_gpx -> {
|
||||
viewGpxTrack()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_share_gpx -> {
|
||||
shareGpxTrack()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_dev_inspect_file -> {
|
||||
val intent = Intent(requireContext(), FitViewerActivity::class.java).apply {
|
||||
putExtra(FitViewerActivity.EXTRA_PATH, workout.summary.rawDetailsPath)
|
||||
}
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_dev_share_raw_summary -> {
|
||||
shareRawSummary(workout)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_dev_share_raw_details -> {
|
||||
shareRawDetails(workout)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_dev_share_json_details -> {
|
||||
shareJsonDetails(workout)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_summary_detail_action_edit_name -> {
|
||||
currentWorkout?.let {
|
||||
workoutEditor.editWorkoutName(it, object : WorkoutEditor.Callback {
|
||||
override fun onWorkoutUpdated() {
|
||||
updateWorkoutHeader(workout.summary)
|
||||
}
|
||||
})
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_summary_detail_action_edit_gps -> {
|
||||
currentWorkout?.let {
|
||||
workoutEditor.editGpsTrack(it, object : WorkoutEditor.Callback {
|
||||
override fun onWorkoutUpdated() {
|
||||
if (workoutHasGps(workout.summary)) {
|
||||
showGpsCanvas()
|
||||
gpsFragment?.setTrackData(ActivitySummaryUtils.getTrackFile(it.summary))
|
||||
} else {
|
||||
hideGpsCanvas()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
requireActivity().finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMenuItems(menu: Menu) {
|
||||
val workout = currentWorkout ?: return
|
||||
|
||||
val hasGpx = workoutHasGps(workout.summary)
|
||||
val hasRawSummary = workout.summary.rawSummaryData != null
|
||||
val hasRawDetails = workout.summary.rawDetailsPath?.let { File(it).exists() } ?: false
|
||||
|
||||
val overflowMenu = menu.findItem(R.id.activity_detail_overflowMenu)?.subMenu
|
||||
if (overflowMenu != null) {
|
||||
overflowMenu.findItem(R.id.activity_action_show_gpx)?.isVisible = hasGpx
|
||||
overflowMenu.findItem(R.id.activity_action_share_gpx)?.isVisible = hasGpx
|
||||
overflowMenu.findItem(R.id.activity_action_dev_inspect_file)?.isVisible =
|
||||
hasRawDetails && workout.summary.rawDetailsPath?.lowercase(Locale.ROOT)?.endsWith(".fit") == true
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_summary)?.isVisible = hasRawSummary
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_details)?.isVisible = hasRawDetails
|
||||
|
||||
val devToolsMenu = overflowMenu.findItem(R.id.activity_action_dev_tools)
|
||||
val devToolsSubMenu = devToolsMenu?.subMenu
|
||||
devToolsMenu?.isVisible = devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun takeSharedScreenshot() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val workout = currentWorkout ?: return@launch
|
||||
val width = binding.scrollView.getChildAt(0).width
|
||||
val height = binding.scrollView.getChildAt(0).height
|
||||
val bitmap = createBitmap(width, height)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
canvas.drawColor(GBApplication.getWindowBackgroundColor(requireContext()))
|
||||
binding.scrollView.draw(canvas)
|
||||
}
|
||||
|
||||
val fileName = FileUtils.makeValidFileName(
|
||||
"Screenshot-${
|
||||
ActivityKind.fromCode(workout.summary.activityKind).getLabel(requireContext()).lowercase()
|
||||
}-${DateTimeUtils.formatIso8601(workout.summary.startTime)}.png"
|
||||
)
|
||||
val targetFile = File(FileUtils.getExternalFilesDir(), fileName)
|
||||
val fOut = FileOutputStream(targetFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut)
|
||||
fOut.flush()
|
||||
fOut.close()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
shareScreenshot(targetFile)
|
||||
GB.toast(requireContext(), "Screenshot saved", Toast.LENGTH_LONG, GB.INFO)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
LOG.error("Error taking screenshot", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareScreenshot(targetFile: File) {
|
||||
val subject = currentWorkout?.summary?.name?.let { "Sports Activity" }
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.screenshot_provider",
|
||||
targetFile
|
||||
)
|
||||
val sharingIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "image/*"
|
||||
putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
putExtra(Intent.EXTRA_TEXT, subject)
|
||||
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(sharingIntent, "Share via"))
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to share screenshot", e)
|
||||
Toast.makeText(requireContext(), R.string.activity_error_no_app_for_png, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewGpxTrack() {
|
||||
val workout = currentWorkout ?: return
|
||||
val gpxFile = ActivitySummaryUtils.getGpxFile(workout.summary)
|
||||
|
||||
if (gpxFile == null) {
|
||||
GB.toast(requireContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
AndroidUtils.viewFile(gpxFile.path, "application/gpx+xml", requireContext())
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
requireContext(),
|
||||
"Unable to display GPX track: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareGpxTrack() {
|
||||
val workout = currentWorkout ?: return
|
||||
val gpxFile = ActivitySummaryUtils.getGpxFile(workout.summary)
|
||||
|
||||
if (gpxFile == null) {
|
||||
GB.toast(requireContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
AndroidUtils.shareFile(requireContext(), gpxFile)
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
requireContext(),
|
||||
"Unable to share GPX track: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareRawSummary(workout: Workout) {
|
||||
if (workout.summary.rawSummaryData == null) {
|
||||
GB.toast(requireContext(), "No raw summary in this activity", Toast.LENGTH_LONG, GB.WARN)
|
||||
return
|
||||
}
|
||||
|
||||
val filename =
|
||||
FileUtils.makeValidFileName("${DateTimeUtils.formatIso8601(workout.summary.startTime)}_summary.bin")
|
||||
|
||||
try {
|
||||
AndroidUtils.shareBytesAsFile(requireContext(), filename, workout.summary.rawSummaryData)
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
requireContext(),
|
||||
"Unable to share raw summary: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareRawDetails(workout: Workout) {
|
||||
if (workout.summary.rawDetailsPath == null) {
|
||||
GB.toast(requireContext(), "No raw details in this activity", Toast.LENGTH_LONG, GB.WARN)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
AndroidUtils.shareFile(requireContext(), File(workout.summary.rawDetailsPath))
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
requireContext(),
|
||||
"Unable to share raw details: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareJsonDetails(workout: Workout) {
|
||||
val filename = FileUtils.makeValidFileName("${DateTimeUtils.formatIso8601(workout.summary.startTime)}.json")
|
||||
|
||||
try {
|
||||
AndroidUtils.shareBytesAsFile(
|
||||
requireContext(),
|
||||
filename,
|
||||
workout.data.toString().toByteArray(StandardCharsets.UTF_8)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
requireContext(),
|
||||
"Unable to share json details: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGBDevice(device: nodomain.freeyourgadget.gadgetbridge.entities.Device?): GBDevice? {
|
||||
return device?.let { findDevice ->
|
||||
GBApplication.app().deviceManager.devices
|
||||
.firstOrNull { it.address.equals(findDevice.identifier, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WorkoutDetailsFragment::class.java)
|
||||
|
||||
private const val ARG_WORKOUT_ID = "workout_id"
|
||||
|
||||
fun newInstance(workoutId: Long): WorkoutDetailsFragment {
|
||||
return WorkoutDetailsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putLong(ARG_WORKOUT_ID, workoutId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.Date
|
||||
|
||||
class WorkoutDetailsViewModel : ViewModel() {
|
||||
private val _workouts = MutableLiveData<List<BaseActivitySummary>>()
|
||||
val workouts: LiveData<List<BaseActivitySummary>> = _workouts
|
||||
|
||||
private val _currentPosition = MutableLiveData<Int>()
|
||||
val currentPosition: LiveData<Int> = _currentPosition
|
||||
|
||||
private val _isLoading = MutableLiveData<Boolean>()
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
|
||||
private val _error = MutableLiveData<String?>()
|
||||
val error: LiveData<String?> = _error
|
||||
|
||||
suspend fun loadSingleWorkout(workoutId: Long) {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
|
||||
try {
|
||||
val workout = withContext(Dispatchers.IO) {
|
||||
GBApplication.acquireDB().use { dbHandler ->
|
||||
val summaryDao = dbHandler.daoSession.baseActivitySummaryDao
|
||||
summaryDao.load(workoutId)
|
||||
}
|
||||
}
|
||||
|
||||
if (workout != null) {
|
||||
_workouts.value = listOf(workout)
|
||||
_currentPosition.value = 0
|
||||
} else {
|
||||
_error.value = "Workout not found"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error loading single workout", e)
|
||||
_error.value = "Failed to load workout: ${e.message}"
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filtered workouts for paging
|
||||
*/
|
||||
suspend fun loadFilteredWorkouts(
|
||||
gbDevice: GBDevice?,
|
||||
activityKindFilter: Int,
|
||||
dateFromFilter: Long,
|
||||
dateToFilter: Long,
|
||||
nameContainsFilter: String?,
|
||||
deviceFilter: Long,
|
||||
itemsFilter: List<Long>?,
|
||||
initialPosition: Int
|
||||
) {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
|
||||
try {
|
||||
val workouts = withContext(Dispatchers.IO) {
|
||||
loadWorkoutsFromDatabase(
|
||||
gbDevice,
|
||||
activityKindFilter,
|
||||
dateFromFilter,
|
||||
dateToFilter,
|
||||
nameContainsFilter,
|
||||
deviceFilter,
|
||||
itemsFilter
|
||||
)
|
||||
}
|
||||
|
||||
_workouts.value = workouts
|
||||
|
||||
// Set initial position, but ensure it's valid
|
||||
val validPosition = if (initialPosition > 0 && initialPosition < workouts.size) {
|
||||
initialPosition
|
||||
} else {
|
||||
0
|
||||
}
|
||||
_currentPosition.value = validPosition
|
||||
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error loading filtered workouts", e)
|
||||
_error.value = "Failed to load workouts: ${e.message}"
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWorkoutsFromDatabase(
|
||||
gbDevice: GBDevice?,
|
||||
activityKindFilter: Int,
|
||||
dateFromFilter: Long,
|
||||
dateToFilter: Long,
|
||||
nameContainsFilter: String?,
|
||||
deviceFilter: Long,
|
||||
itemsFilter: List<Long>?
|
||||
): List<BaseActivitySummary> {
|
||||
return GBApplication.acquireDB().use { dbHandler ->
|
||||
val summaryDao = dbHandler.daoSession.baseActivitySummaryDao
|
||||
val dbDevice = gbDevice?.let { DBHelper.findDevice(it, dbHandler.daoSession) }
|
||||
|
||||
val queryBuilder = summaryDao.queryBuilder()
|
||||
|
||||
// Apply device filter
|
||||
when {
|
||||
deviceFilter != 0L && deviceFilter != ALL_DEVICES -> {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceFilter))
|
||||
}
|
||||
dbDevice != null -> {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.DeviceId.eq(dbDevice.id))
|
||||
}
|
||||
}
|
||||
queryBuilder.orderDesc(BaseActivitySummaryDao.Properties.StartTime)
|
||||
|
||||
if (activityKindFilter != 0) {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.ActivityKind.eq(activityKindFilter))
|
||||
}
|
||||
|
||||
if (dateFromFilter != 0L) {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.StartTime.gt(Date(dateFromFilter)))
|
||||
}
|
||||
if (dateToFilter != 0L) {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.EndTime.lt(Date(dateToFilter)))
|
||||
}
|
||||
|
||||
if (!nameContainsFilter.isNullOrEmpty()) {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.Name.like("%${nameContainsFilter}%"))
|
||||
}
|
||||
|
||||
if (!itemsFilter.isNullOrEmpty()) {
|
||||
queryBuilder.where(BaseActivitySummaryDao.Properties.Id.`in`(itemsFilter))
|
||||
}
|
||||
|
||||
queryBuilder.list()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentPosition(position: Int) {
|
||||
_currentPosition.value = position
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WorkoutDetailsViewModel::class.java)
|
||||
|
||||
const val ALL_DEVICES = 999L
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class WorkoutEditor(private val context: Context) {
|
||||
var filesGpxList: MutableList<String> = ArrayList()
|
||||
var selectedGpxIndex: Int = 0
|
||||
var selectedGpxFile: String? = null
|
||||
var exportPathRoot: File? = null // original gpx file location, could still contain old gpx files
|
||||
var exportPathGpx: File? = null // new gpx file location
|
||||
|
||||
fun editWorkoutName(workout: Workout, callback: Callback) {
|
||||
val input = EditText(context).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
setText(workout.summary.name ?: "")
|
||||
}
|
||||
|
||||
val container = FrameLayout(context)
|
||||
val params = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
leftMargin = context.resources.getDimensionPixelSize(R.dimen.dialog_margin)
|
||||
rightMargin = context.resources.getDimensionPixelSize(R.dimen.dialog_margin)
|
||||
}
|
||||
input.layoutParams = params
|
||||
container.addView(input)
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setView(container)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.activity_summary_edit_name_title)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val newName = input.text.toString().takeIf { it.isNotEmpty() }
|
||||
workout.summary.name = newName
|
||||
workout.summary.update()
|
||||
callback.onWorkoutUpdated()
|
||||
}
|
||||
.setNegativeButton(R.string.Cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun buildGpxFileList(): MutableList<String> {
|
||||
val files: MutableList<File> = ArrayList()
|
||||
val gpxFileFilter = FileFilter { file: File? ->
|
||||
file!!.isFile() && file.path.lowercase(Locale.getDefault()).endsWith(".gpx")
|
||||
}
|
||||
exportPathRoot?.let {
|
||||
val rootFiles: Array<File?>? = it.listFiles(gpxFileFilter)
|
||||
if (rootFiles != null) Collections.addAll<File?>(files, *rootFiles)
|
||||
}
|
||||
exportPathGpx?.let {
|
||||
val gpxFiles: Array<File?>? = it.listFiles(gpxFileFilter)
|
||||
if (gpxFiles != null) Collections.addAll<File?>(files, *gpxFiles)
|
||||
}
|
||||
|
||||
files.sortWith { file1: File?, file2: File? ->
|
||||
val lastModified1 = file1!!.lastModified()
|
||||
val lastModified2 = file2!!.lastModified()
|
||||
lastModified2.compareTo(lastModified1) // Descending order
|
||||
}
|
||||
|
||||
val list: MutableList<String> = ArrayList()
|
||||
list.add(context.getString(R.string.activity_summary_detail_clear_gpx_track))
|
||||
for (file in files) {
|
||||
list.add(file.getName())
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private fun getPath(): File? {
|
||||
var path: File? = null
|
||||
try {
|
||||
path = FileUtils.getExternalFilesDir()
|
||||
} catch (e: IOException) {
|
||||
LOG.error("Error getting path", e)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
fun editGpsTrack(workout: Workout, callback: Callback) {
|
||||
exportPathRoot = getPath()
|
||||
exportPathGpx = File(getPath(), "gpx")
|
||||
filesGpxList = buildGpxFileList()
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(R.string.activity_summary_detail_select_gpx_track)
|
||||
val directoryListing = ArrayAdapter(context, android.R.layout.simple_list_item_1, filesGpxList)
|
||||
builder.setSingleChoiceItems(directoryListing, 0) { dialog: DialogInterface?, which: Int ->
|
||||
selectedGpxIndex = which
|
||||
val selectedFilename = filesGpxList[selectedGpxIndex]
|
||||
if (File(exportPathGpx, selectedFilename).isFile()) {
|
||||
// Note: if selectedFilename exists in both exportPathGpx and exportPathRoot,
|
||||
// this code will always choose the one in exportPathGpx. This is acceptable
|
||||
// because exportPathGpx is where all new files end up, and gpx files tend to
|
||||
// have a unique name anyway because it usually contains some timestamp.
|
||||
selectedGpxFile = File(exportPathGpx, selectedFilename).getPath()
|
||||
} else {
|
||||
selectedGpxFile = File(exportPathRoot, selectedFilename).getPath()
|
||||
}
|
||||
var message = String.format("%s %s?", context.getString(R.string.set), filesGpxList.get(selectedGpxIndex))
|
||||
if (selectedGpxIndex == 0) {
|
||||
selectedGpxFile = null
|
||||
message = String.format("%s?", context.getString(R.string.activity_summary_detail_clear_gpx_track))
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setCancelable(true)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setTitle(R.string.activity_summary_detail_editing_gpx_track)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok) { dialog1, which1 ->
|
||||
workout.summary.gpxTrack = selectedGpxFile
|
||||
workout.summary.update()
|
||||
callback.onWorkoutUpdated()
|
||||
}
|
||||
.setNegativeButton(R.string.Cancel) { dialog2, which2 -> }
|
||||
.show()
|
||||
dialog!!.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onWorkoutUpdated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WorkoutEditor::class.java)
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackViewModel
|
||||
import nodomain.freeyourgadget.gadgetbridge.databinding.FragmentWorkoutGpsBinding
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
|
||||
class WorkoutGpsFragment : Fragment() {
|
||||
private val viewModel: MapsTrackViewModel by viewModels()
|
||||
private lateinit var binding: FragmentWorkoutGpsBinding
|
||||
|
||||
private lateinit var mapsManager: MapsManager
|
||||
private var mapValid = true
|
||||
|
||||
private var inputFile: File? = null
|
||||
|
||||
private val mReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (MapsSettingsFragment.ACTION_SETTING_CHANGE == intent.action) {
|
||||
if (!isResumed) {
|
||||
mapValid = false
|
||||
} else {
|
||||
mapsManager.reload()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
LOG.warn("Unknown action {}", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentWorkoutGpsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.mapView.setBuiltInZoomControls(false)
|
||||
|
||||
mapsManager = MapsManager(requireContext(), binding.mapView)
|
||||
mapsManager.loadMaps()
|
||||
if (mapsManager.isMapLoaded) {
|
||||
binding.missingMapsWarning.visibility = View.GONE
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(mReceiver, IntentFilter().apply {
|
||||
addAction(MapsSettingsFragment.ACTION_SETTING_CHANGE)
|
||||
})
|
||||
|
||||
inputFile?.let { viewModel.loadTrackData(it) }
|
||||
|
||||
observeViewModel()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.trackPoints.observe(viewLifecycleOwner) { points ->
|
||||
if (points.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
drawTrack(points)
|
||||
}
|
||||
} else {
|
||||
LOG.warn("No track points to display or file was empty.")
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
showLoading(isLoading)
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner) { error ->
|
||||
error?.let {
|
||||
showError(it.localizedMessage ?: it.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!mapValid) {
|
||||
mapsManager.reload()
|
||||
mapValid = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(mReceiver)
|
||||
mapsManager.onDestroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
fun setTrackData(inputFile: File?) {
|
||||
this.inputFile = inputFile
|
||||
if (mapsManager.isMapLoaded && inputFile != null) {
|
||||
viewModel.loadTrackData(inputFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoading(isLoading: Boolean) {
|
||||
binding.loadingSpinner.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.mapView.visibility = if (isLoading) View.INVISIBLE else View.VISIBLE
|
||||
binding.errorMessage.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
binding.loadingSpinner.visibility = View.GONE
|
||||
binding.mapView.visibility = View.INVISIBLE
|
||||
binding.errorMessage.visibility = View.VISIBLE
|
||||
binding.errorMessage.text = message
|
||||
}
|
||||
|
||||
private suspend fun drawTrack(trackPoints: List<GPSCoordinate>) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
mapsManager.setTrack(trackPoints)
|
||||
|
||||
setupMapTouchListener()
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Error drawing track", e)
|
||||
showError(e.localizedMessage ?: e.message ?: "Error drawing track")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMapTouchListener() {
|
||||
binding.mapView.setOnTouchListener(object : View.OnTouchListener {
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var pressTime = 0L
|
||||
private var isDrag = false
|
||||
|
||||
private val TAP_THRESHOLD = 10
|
||||
private val LONG_PRESS_TIME = 300
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
isDrag = false
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
pressTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (kotlin.math.abs(startX - event.x) > TAP_THRESHOLD ||
|
||||
kotlin.math.abs(startY - event.y) > TAP_THRESHOLD
|
||||
) {
|
||||
isDrag = true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (!isDrag && System.currentTimeMillis() - pressTime < LONG_PRESS_TIME) {
|
||||
openMapActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun openMapActivity() {
|
||||
val intent = Intent(getContext(), MapsTrackActivity::class.java)
|
||||
intent.putExtra("file", inputFile)
|
||||
startActivity(intent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WorkoutGpsFragment::class.java)
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
|
||||
|
||||
class WorkoutViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
|
||||
private var workouts: List<BaseActivitySummary> = emptyList()
|
||||
|
||||
override fun getItemCount(): Int = workouts.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return WorkoutDetailsFragment.newInstance(workouts[position].id)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return workouts[position].id
|
||||
}
|
||||
|
||||
override fun containsItem(itemId: Long): Boolean {
|
||||
return workouts.any { it.id == itemId }
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateWorkouts(newWorkouts: List<BaseActivitySummary>) {
|
||||
workouts = newWorkouts
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
|
||||
object ActivitySummaryGroup {
|
||||
fun buildGroupedList(activitySummaryData: ActivitySummaryData): Map<String, List<Pair<String, ActivitySummaryEntry>>> {
|
||||
// Initialize activeGroups with the initial expected order and empty arrays
|
||||
val activeGroups = DEFAULT_GROUPS.keys
|
||||
.associateWith { mutableListOf<Pair<String, ActivitySummaryEntry>>() }
|
||||
.toMutableMap()
|
||||
|
||||
activitySummaryData.keys
|
||||
.filterNot { it.startsWith("internal") }
|
||||
.forEach { key ->
|
||||
val item = activitySummaryData[key]
|
||||
// Use the group if specified in the entry, otherwise fallback to the default mapping from getDefaultGroup
|
||||
val groupName = item.group ?: getDefaultGroup(key)
|
||||
|
||||
// If the group is not defined the default groups, it will be added to the end
|
||||
val group = activeGroups.getOrPut(groupName) { mutableListOf() }
|
||||
group.add(Pair.of<String, ActivitySummaryEntry>(key, item))
|
||||
}
|
||||
|
||||
// activeGroups is already ordered, discard empty ones
|
||||
return activeGroups.filterValues { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the default group key for a given entry. Defaults to Activity if not found.
|
||||
*/
|
||||
private fun getDefaultGroup(searchItem: String): String {
|
||||
return DEFAULT_GROUPS.entries
|
||||
.firstOrNull { (_, items) -> items.contains(searchItem) }
|
||||
?.key
|
||||
?: ActivitySummaryEntries.GROUP_ACTIVITY
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of group key to list of entries that should be in that group by default.
|
||||
*/
|
||||
val DEFAULT_GROUPS: Map<String, List<String>> = object : LinkedHashMap<String, List<String>>() {
|
||||
init {
|
||||
// NB: Default group Activity must be present in this definition, otherwise it wouldn't
|
||||
// be shown.
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_ACTIVITY, listOf<String>(
|
||||
ActivitySummaryEntries.DISTANCE_METERS,
|
||||
ActivitySummaryEntries.STEPS,
|
||||
ActivitySummaryEntries.STEP_RATE_SUM,
|
||||
ActivitySummaryEntries.ACTIVE_SECONDS,
|
||||
ActivitySummaryEntries.CALORIES_BURNT,
|
||||
ActivitySummaryEntries.STRIDE_TOTAL,
|
||||
ActivitySummaryEntries.HR_AVG,
|
||||
ActivitySummaryEntries.HR_MAX,
|
||||
ActivitySummaryEntries.HR_MIN,
|
||||
ActivitySummaryEntries.STRIDE_AVG,
|
||||
ActivitySummaryEntries.STRIDE_MAX,
|
||||
ActivitySummaryEntries.STRIDE_MIN,
|
||||
ActivitySummaryEntries.STEP_LENGTH_AVG,
|
||||
ActivitySummaryEntries.STANDING_TIME,
|
||||
ActivitySummaryEntries.STANDING_COUNT,
|
||||
ActivitySummaryEntries.AVG_POWER,
|
||||
ActivitySummaryEntries.MAX_POWER,
|
||||
ActivitySummaryEntries.NORMALIZED_POWER,
|
||||
)
|
||||
)
|
||||
|
||||
// Speed
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_SPEED, listOf<String>(
|
||||
ActivitySummaryEntries.SPEED_AVG,
|
||||
ActivitySummaryEntries.SPEED_MAX,
|
||||
ActivitySummaryEntries.SPEED_MIN,
|
||||
ActivitySummaryEntries.PACE_AVG_SECONDS_KM,
|
||||
ActivitySummaryEntries.PACE_MIN,
|
||||
ActivitySummaryEntries.PACE_MAX,
|
||||
"averageSpeed2",
|
||||
ActivitySummaryEntries.CADENCE_AVG,
|
||||
ActivitySummaryEntries.CADENCE_MAX,
|
||||
ActivitySummaryEntries.CADENCE_MIN,
|
||||
ActivitySummaryEntries.STEP_RATE_AVG,
|
||||
ActivitySummaryEntries.STEP_RATE_MAX,
|
||||
)
|
||||
)
|
||||
|
||||
// Elevation
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_ELEVATION, listOf<String>(
|
||||
ActivitySummaryEntries.TOTAL_ASCENT,
|
||||
ActivitySummaryEntries.TOTAL_DESCENT,
|
||||
ActivitySummaryEntries.ASCENT_METERS,
|
||||
ActivitySummaryEntries.DESCENT_METERS,
|
||||
ActivitySummaryEntries.ALTITUDE_MAX,
|
||||
ActivitySummaryEntries.ALTITUDE_MIN,
|
||||
ActivitySummaryEntries.ALTITUDE_AVG,
|
||||
ActivitySummaryEntries.ALTITUDE_BASE,
|
||||
ActivitySummaryEntries.ASCENT_SECONDS,
|
||||
ActivitySummaryEntries.DESCENT_SECONDS,
|
||||
ActivitySummaryEntries.FLAT_SECONDS,
|
||||
ActivitySummaryEntries.ASCENT_DISTANCE,
|
||||
ActivitySummaryEntries.DESCENT_DISTANCE,
|
||||
ActivitySummaryEntries.FLAT_DISTANCE,
|
||||
ActivitySummaryEntries.ELEVATION_GAIN,
|
||||
ActivitySummaryEntries.ELEVATION_LOSS,
|
||||
)
|
||||
)
|
||||
|
||||
// Strokes
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_STROKES, listOf<String>(
|
||||
ActivitySummaryEntries.STROKE_DISTANCE_AVG,
|
||||
ActivitySummaryEntries.STROKE_AVG_PER_SECOND,
|
||||
ActivitySummaryEntries.STROKES,
|
||||
ActivitySummaryEntries.STROKE_RATE_AVG,
|
||||
ActivitySummaryEntries.STROKE_RATE_MAX,
|
||||
)
|
||||
)
|
||||
|
||||
// Jumps
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_JUMPS, listOf<String>(
|
||||
ActivitySummaryEntries.JUMPS,
|
||||
ActivitySummaryEntries.JUMP_RATE_AVG,
|
||||
ActivitySummaryEntries.JUMP_RATE_MAX,
|
||||
)
|
||||
)
|
||||
|
||||
// Swimming
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_SWIMMING, listOf<String>(
|
||||
ActivitySummaryEntries.POOL_LENGTH,
|
||||
ActivitySummaryEntries.SWIM_AVG_CADENCE,
|
||||
ActivitySummaryEntries.SWOLF_INDEX,
|
||||
ActivitySummaryEntries.SWOLF_AVG,
|
||||
ActivitySummaryEntries.SWOLF_MAX,
|
||||
ActivitySummaryEntries.SWOLF_MIN,
|
||||
ActivitySummaryEntries.SWIM_STYLE,
|
||||
)
|
||||
)
|
||||
|
||||
// Cycling
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_CYCLING, listOf<String>(
|
||||
ActivitySummaryEntries.LEFT_RIGHT_BALANCE,
|
||||
ActivitySummaryEntries.AVG_PEDAL_SMOOTHNESS,
|
||||
ActivitySummaryEntries.AVG_TORQUE_EFFECTIVENESS,
|
||||
ActivitySummaryEntries.AVG_LEFT_PCO,
|
||||
ActivitySummaryEntries.AVG_LEFT_POWER_PHASE,
|
||||
ActivitySummaryEntries.AVG_LEFT_POWER_PHASE_PEAK,
|
||||
ActivitySummaryEntries.AVG_RIGHT_PCO,
|
||||
ActivitySummaryEntries.AVG_RIGHT_POWER_PHASE,
|
||||
ActivitySummaryEntries.AVG_RIGHT_POWER_PHASE_PEAK,
|
||||
ActivitySummaryEntries.AVG_POWER_STANDING,
|
||||
ActivitySummaryEntries.MAX_POWER_STANDING,
|
||||
ActivitySummaryEntries.AVG_POWER_SEATING,
|
||||
ActivitySummaryEntries.MAX_POWER_SEATING,
|
||||
ActivitySummaryEntries.AVG_CADENCE_STANDING,
|
||||
ActivitySummaryEntries.AVG_CADENCE_SEATING,
|
||||
ActivitySummaryEntries.MAX_CADENCE_STANDING,
|
||||
ActivitySummaryEntries.MAX_CADENCE_SEATING,
|
||||
ActivitySummaryEntries.FRONT_GEAR_SHIFTS,
|
||||
ActivitySummaryEntries.REAR_GEAR_SHIFTS,
|
||||
)
|
||||
)
|
||||
|
||||
// Training effect
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_TRAINING_EFFECT, listOf<String>(
|
||||
ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC,
|
||||
ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC,
|
||||
ActivitySummaryEntries.WORKOUT_LOAD,
|
||||
ActivitySummaryEntries.TRAINING_LOAD,
|
||||
ActivitySummaryEntries.INTENSITY_FACTOR,
|
||||
ActivitySummaryEntries.TRAINING_STRESS_SCORE,
|
||||
ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE,
|
||||
ActivitySummaryEntries.RECOVERY_TIME,
|
||||
ActivitySummaryEntries.LACTATE_THRESHOLD_HR,
|
||||
)
|
||||
)
|
||||
|
||||
// Laps
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_LAPS, listOf<String>(
|
||||
ActivitySummaryEntries.LAP_PACE_AVERAGE,
|
||||
ActivitySummaryEntries.LAPS,
|
||||
ActivitySummaryEntries.LANE_LENGTH,
|
||||
)
|
||||
)
|
||||
|
||||
// Pace
|
||||
put(ActivitySummaryEntries.GROUP_PACE, listOf<String>())
|
||||
|
||||
// Running form
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_RUNNING_FORM, listOf<String>(
|
||||
ActivitySummaryEntries.GROUND_CONTACT_TIME_AVG,
|
||||
ActivitySummaryEntries.IMPACT_AVG,
|
||||
ActivitySummaryEntries.IMPACT_MAX,
|
||||
ActivitySummaryEntries.SWING_ANGLE_AVG,
|
||||
ActivitySummaryEntries.FORE_FOOT_LANDINGS,
|
||||
ActivitySummaryEntries.MID_FOOT_LANDINGS,
|
||||
ActivitySummaryEntries.BACK_FOOT_LANDINGS,
|
||||
ActivitySummaryEntries.EVERSION_ANGLE_AVG,
|
||||
ActivitySummaryEntries.EVERSION_ANGLE_MAX,
|
||||
)
|
||||
)
|
||||
|
||||
// Heart rate zones
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_HEART_RATE_ZONES, listOf<String>(
|
||||
ActivitySummaryEntries.HR_ZONE_NA,
|
||||
ActivitySummaryEntries.HR_ZONE_WARM_UP,
|
||||
ActivitySummaryEntries.HR_ZONE_FAT_BURN,
|
||||
ActivitySummaryEntries.HR_ZONE_EASY,
|
||||
ActivitySummaryEntries.HR_ZONE_AEROBIC,
|
||||
ActivitySummaryEntries.HR_ZONE_ANAEROBIC,
|
||||
ActivitySummaryEntries.HR_ZONE_THRESHOLD,
|
||||
ActivitySummaryEntries.HR_ZONE_EXTREME,
|
||||
ActivitySummaryEntries.HR_ZONE_MAXIMUM,
|
||||
)
|
||||
)
|
||||
|
||||
// Diving
|
||||
put(
|
||||
ActivitySummaryEntries.GROUP_DIVING, listOf<String>(
|
||||
ActivitySummaryEntries.AVG_DEPTH,
|
||||
ActivitySummaryEntries.MAX_DEPTH,
|
||||
ActivitySummaryEntries.START_CNS,
|
||||
ActivitySummaryEntries.END_CNS,
|
||||
ActivitySummaryEntries.START_N2,
|
||||
ActivitySummaryEntries.END_N2,
|
||||
ActivitySummaryEntries.DIVE_NUMBER,
|
||||
ActivitySummaryEntries.BOTTOM_TIME,
|
||||
)
|
||||
)
|
||||
|
||||
// Sets
|
||||
put(ActivitySummaryEntries.SETS, listOf<String>())
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-16
@@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -34,6 +33,7 @@ import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
@@ -67,14 +67,14 @@ import com.google.gson.GsonBuilder;
|
||||
public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<BaseActivitySummary> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesAdapter.class);
|
||||
private final GBDevice device;
|
||||
long dateFromFilter = 0;
|
||||
long dateToFilter = 0;
|
||||
long dateFromFilter;
|
||||
long dateToFilter;
|
||||
long deviceFilter;
|
||||
String nameContainsFilter;
|
||||
List<Long> itemsFilter;
|
||||
private int activityKindFilter;
|
||||
|
||||
public ActivitySummariesAdapter(Context context, GBDevice device, int activityKindFilter, long dateFromFilter, long dateToFilter, String nameContainsFilter, long deviceFilter, List itemsFilter) {
|
||||
public ActivitySummariesAdapter(Context context, GBDevice device, int activityKindFilter, long dateFromFilter, long dateToFilter, String nameContainsFilter, long deviceFilter, List<Long> itemsFilter) {
|
||||
super(context);
|
||||
this.device = device;
|
||||
this.activityKindFilter = activityKindFilter;
|
||||
@@ -91,6 +91,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
BaseActivitySummaryDao summaryDao = handler.getDaoSession().getBaseActivitySummaryDao();
|
||||
Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
|
||||
assert dbDevice != null;
|
||||
|
||||
QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
|
||||
|
||||
@@ -149,14 +150,13 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
@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);
|
||||
return switch (viewType) {
|
||||
case 0 -> // dashboard
|
||||
new DashboardViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_summary_dashboard_item, parent, false));
|
||||
case 2 -> // item
|
||||
new ActivityItemViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_item, parent, false));
|
||||
default -> super.onCreateViewHolder(parent, viewType);
|
||||
};
|
||||
}
|
||||
|
||||
public void setActivityKindFilter(int filter) {
|
||||
@@ -183,7 +183,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
this.deviceFilter = device;
|
||||
}
|
||||
|
||||
public int gettActivityKindFilter() {
|
||||
public int getActivityKindFilter() {
|
||||
return this.activityKindFilter;
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
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);
|
||||
hasGps = summaryData.getBoolean(INTERNAL_HAS_GPS, false);
|
||||
} else {
|
||||
hasGps = false;
|
||||
}
|
||||
@@ -281,8 +281,8 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
||||
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());
|
||||
if (getActivityKindFilter() != 0) {
|
||||
ActivityKind activityKind = ActivityKind.fromCode(getActivityKindFilter());
|
||||
activityName = activityKind.getLabel(getContext());
|
||||
activityIconView.setImageResource(activityKind.getIcon());
|
||||
activityIconBigView.setImageResource(activityKind.getIcon());
|
||||
|
||||
+126
-15
@@ -1,6 +1,101 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_CADENCE_SEATING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_CADENCE_STANDING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_DEPTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_GROUND_CONTACT_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_GROUND_CONTACT_TIME_BALANCE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_LEFT_PCO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_LEFT_POWER_PHASE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_LEFT_POWER_PHASE_PEAK;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_PEDAL_SMOOTHNESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_POWER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_POWER_SEATING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_POWER_STANDING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_RIGHT_PCO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_RIGHT_POWER_PHASE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_RIGHT_POWER_PHASE_PEAK;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_TORQUE_EFFECTIVENESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_VERTICAL_OSCILLATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.AVG_VERTICAL_RATIO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.BOTTOM_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_ACTIVE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_RESTING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DIVE_NUMBER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.END_CNS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.END_N2;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ESTIMATED_SWEAT_LOSS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.FRONT_GEAR_SHIFTS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.GAS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.GROUP_INTERVALS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HRV_RMSSD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HRV_SDRR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EASY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_MAXIMUM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_NA;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_THRESHOLD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTENSITY_FACTOR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LACTATE_THRESHOLD_HR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LEFT_RIGHT_BALANCE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_CADENCE_SEATING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_CADENCE_STANDING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_DEPTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_POWER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_POWER_SEATING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAX_POWER_STANDING;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.NORMALIZED_POWER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_AVG_SECONDS_KM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_MAX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.POOL_LENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.REAR_GEAR_SHIFTS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RESPIRATION_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RESPIRATION_MAX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RESPIRATION_MIN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SETS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MAX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPO2_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STANDING_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STANDING_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.START_CNS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.START_N2;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STEP_LENGTH_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRESS_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWIM_AVG_CADENCE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWOLF_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TOTAL_ASCENT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TOTAL_DESCENT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_LOAD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_STRESS_SCORE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BREATHS_PER_MIN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LB;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MILLISECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_PERCENTAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_RPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STROKES_PER_LENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_WATT;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -14,7 +109,6 @@ import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@@ -25,8 +119,9 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
|
||||
@@ -34,6 +129,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionExerciseCategory.ExerciseCategory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveGas;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveSummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitLap;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
||||
@@ -42,7 +138,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitUserProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveSummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
@@ -67,10 +162,16 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
|
||||
public BaseActivitySummary parseBinaryData(BaseActivitySummary summary, boolean forDetails) {
|
||||
// FIXME Do not use this
|
||||
return parseWorkout(summary, forDetails).getSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Workout parseWorkout(BaseActivitySummary summary, final boolean forDetails) {
|
||||
if (!forDetails) {
|
||||
// Our parsing is too slow, especially without a RecyclerView
|
||||
return summary;
|
||||
return new Workout(summary, ActivitySummaryData.fromJson(summary.getSummaryData()));
|
||||
}
|
||||
|
||||
final long nanoStart = System.nanoTime();
|
||||
@@ -80,12 +181,12 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
final String rawDetailsPath = summary.getRawDetailsPath();
|
||||
if (rawDetailsPath == null) {
|
||||
LOG.warn("No rawDetailsPath");
|
||||
return summary;
|
||||
return new Workout(summary, ActivitySummaryData.fromJson(summary.getSummaryData()));
|
||||
}
|
||||
final File file = FileUtils.tryFixPath(new File(rawDetailsPath));
|
||||
if (file == null || !file.isFile() || !file.canRead()) {
|
||||
LOG.warn("Unable to read {}", rawDetailsPath);
|
||||
return summary;
|
||||
return new Workout(summary, ActivitySummaryData.fromJson(summary.getSummaryData()));
|
||||
}
|
||||
|
||||
final FitFile fitFile;
|
||||
@@ -93,20 +194,28 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
fitFile = FitFile.parseIncoming(file);
|
||||
} catch (final IOException | FitParseException e) {
|
||||
LOG.error("Failed to parse fit file", e);
|
||||
return summary;
|
||||
return new Workout(summary, ActivitySummaryData.fromJson(summary.getSummaryData()));
|
||||
}
|
||||
|
||||
for (final RecordData record : fitFile.getRecords()) {
|
||||
handleRecord(record);
|
||||
}
|
||||
|
||||
updateSummary(summary);
|
||||
final ActivitySummaryData activitySummaryData = updateSummary(summary);
|
||||
|
||||
final List<WorkoutChart> charts = new LinkedList<>();
|
||||
|
||||
// TODO charts
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
LOG.trace("Updating summary took {}ms", executionTime);
|
||||
|
||||
return summary;
|
||||
return new Workout(
|
||||
summary,
|
||||
activitySummaryData,
|
||||
charts
|
||||
);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
@@ -177,14 +286,14 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateSummary(final BaseActivitySummary summary) {
|
||||
public ActivitySummaryData updateSummary(final BaseActivitySummary summary) {
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
if (session == null) {
|
||||
LOG.error("Got workout, but no session");
|
||||
return;
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
final ActivityKind activityKind;
|
||||
if (sport != null) {
|
||||
if (StringUtils.isNullOrEmpty(summary.getName())) {
|
||||
@@ -660,6 +769,8 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
);
|
||||
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
public Number safeRound(final Number number) {
|
||||
|
||||
+2
-2
@@ -140,10 +140,10 @@ public class ActivitySummaryData {
|
||||
return Boolean.parseBoolean((String) value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
public static ActivitySummaryData fromJson(final String string) {
|
||||
if (StringUtils.isBlank(string)) {
|
||||
return null;
|
||||
return new ActivitySummaryData();
|
||||
}
|
||||
|
||||
final Type type = new TypeToken<LinkedHashMap<String, ActivitySummaryEntry>>(){}.getType();
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/* Copyright (C) 2020-2024 Petr Vaněk
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class ActivitySummaryItems {
|
||||
ActivitySummariesAdapter itemsAdapter;
|
||||
private int current_position = 0;
|
||||
|
||||
public ActivitySummaryItems(Context context, GBDevice device, int activityKindFilter, long dateFromFilter, long dateToFilter, String nameContainsFilter, long deviceFilter, List itemsFilter) {
|
||||
|
||||
this.itemsAdapter = new ActivitySummariesAdapter(context, device, activityKindFilter, dateFromFilter, dateToFilter, nameContainsFilter, deviceFilter, itemsFilter);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BaseActivitySummary getItem(int position) {
|
||||
if (position == 0) return null;
|
||||
current_position = position;
|
||||
return itemsAdapter.getItem(position);
|
||||
}
|
||||
|
||||
public int getPosition(BaseActivitySummary item) {
|
||||
return itemsAdapter.getPosition(item);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BaseActivitySummary getNextItem() {
|
||||
// last one is empty to avoid items behind fab
|
||||
if (current_position + 2 < itemsAdapter.getItemCount()) {
|
||||
current_position += 1;
|
||||
return itemsAdapter.getItem(current_position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BaseActivitySummary getPrevItem() {
|
||||
if (current_position - 1 >= 1) { //0 is empty item for summary dashboard
|
||||
current_position -= 1;
|
||||
return itemsAdapter.getItem(current_position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-158
@@ -19,25 +19,14 @@ package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
|
||||
public class ActivitySummaryJsonSummary {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryJsonSummary.class);
|
||||
private Map<String, List<String>> groupData;
|
||||
private ActivitySummaryData summaryData;
|
||||
private Map<String, List<Pair<String, ActivitySummaryEntry>>> summaryGroupedList;
|
||||
private final ActivitySummaryParser summaryParser;
|
||||
private final BaseActivitySummary baseActivitySummary;
|
||||
|
||||
@@ -48,9 +37,6 @@ public class ActivitySummaryJsonSummary {
|
||||
|
||||
private ActivitySummaryData setSummaryData(BaseActivitySummary item, final boolean forDetails) {
|
||||
final ActivitySummaryData summary = ActivitySummaryData.fromJson(getCorrectSummary(item, forDetails));
|
||||
if (summary == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//add additionally computed values here
|
||||
if (item.getBaseAltitude() != null && item.getBaseAltitude() != -20000 && !summary.has("baseAltitude")) {
|
||||
@@ -89,148 +75,4 @@ public class ActivitySummaryJsonSummary {
|
||||
}
|
||||
return item.getSummaryData();
|
||||
}
|
||||
|
||||
public Map<String, List<Pair<String, ActivitySummaryEntry>>> getSummaryGroupedList() {
|
||||
//returns list grouped by activity groups as per createActivitySummaryGroups
|
||||
if (summaryData == null) summaryData = setSummaryData(baseActivitySummary, true);
|
||||
if (summaryGroupedList == null) summaryGroupedList = setSummaryGroupedList(summaryData);
|
||||
return summaryGroupedList;
|
||||
}
|
||||
|
||||
private Map<String, List<Pair<String, ActivitySummaryEntry>>> setSummaryGroupedList(ActivitySummaryData activitySummaryData) {
|
||||
this.groupData = createActivitySummaryGroups(); //structure for grouping activities into groups, when visualizing
|
||||
|
||||
if (activitySummaryData == null) return null;
|
||||
Iterator<String> keys = activitySummaryData.getKeys().iterator();
|
||||
|
||||
final Map<String, List<Pair<String, ActivitySummaryEntry>>> activeGroups = new LinkedHashMap<>();
|
||||
// Initialize activeGroups with the initial expected order and empty arrays
|
||||
for (final String key : this.groupData.keySet()) {
|
||||
activeGroups.put(key, new LinkedList<>());
|
||||
}
|
||||
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
if (key.startsWith("internal")) {
|
||||
continue;
|
||||
}
|
||||
ActivitySummaryEntry item = activitySummaryData.get(key);
|
||||
// Use the group if specified in the entry, otherwise fallback to the array below
|
||||
String groupName = item.getGroup() != null ? item.getGroup() : getDefaultGroup(key);
|
||||
|
||||
List<Pair<String, ActivitySummaryEntry>> group = activeGroups.get(groupName);
|
||||
if (group == null) {
|
||||
// This group is not defined in createActivitySummaryGroups - add it to the end
|
||||
group = new LinkedList<>();
|
||||
activeGroups.put(groupName, group);
|
||||
}
|
||||
|
||||
group.add(Pair.of(key, item));
|
||||
}
|
||||
|
||||
// Convert activeGroups to the expected JSONObject
|
||||
// activeGroups is already ordered
|
||||
final Map<String, List<Pair<String, ActivitySummaryEntry>>> grouped = new LinkedHashMap<>();
|
||||
for (final Map.Entry<String, List<Pair<String, ActivitySummaryEntry>>> entry : activeGroups.entrySet()) {
|
||||
if (entry.getValue().isEmpty()) {
|
||||
// empty group
|
||||
continue;
|
||||
}
|
||||
grouped.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private String getDefaultGroup(final String searchItem) {
|
||||
final String defaultGroup = GROUP_ACTIVITY;
|
||||
if (groupData == null) return defaultGroup;
|
||||
for (final String groupKey : groupData.keySet()) {
|
||||
final List<String> itemList = groupData.get(groupKey);
|
||||
if (itemList == null) {
|
||||
continue;
|
||||
}
|
||||
for (final String itemKey : itemList) {
|
||||
if (itemKey.equals(searchItem)) {
|
||||
return groupKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Default group must be present in group JSONObject created by createActivitySummaryGroups
|
||||
return defaultGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection ArraysAsListWithZeroOrOneArgument
|
||||
*/
|
||||
private static Map<String, List<String>> createActivitySummaryGroups() {
|
||||
return new LinkedHashMap<String, List<String>>() {{
|
||||
// NB: Default group Activity must be present in this definition, otherwise it wouldn't
|
||||
// be shown.
|
||||
put(GROUP_ACTIVITY, Arrays.asList(
|
||||
DISTANCE_METERS, STEPS, STEP_RATE_SUM, ACTIVE_SECONDS, CALORIES_BURNT,
|
||||
STRIDE_TOTAL, HR_AVG, HR_MAX, HR_MIN, STRIDE_AVG, STRIDE_MAX, STRIDE_MIN,
|
||||
STEP_LENGTH_AVG,
|
||||
STANDING_TIME, STANDING_COUNT,
|
||||
AVG_POWER, MAX_POWER, NORMALIZED_POWER
|
||||
));
|
||||
put(GROUP_SPEED, Arrays.asList(
|
||||
SPEED_AVG, SPEED_MAX, SPEED_MIN, PACE_AVG_SECONDS_KM, PACE_MIN,
|
||||
PACE_MAX, "averageSpeed2", CADENCE_AVG, CADENCE_MAX, CADENCE_MIN,
|
||||
STEP_RATE_AVG, STEP_RATE_MAX
|
||||
));
|
||||
put(GROUP_ELEVATION, Arrays.asList(
|
||||
ASCENT_METERS, DESCENT_METERS, ALTITUDE_MAX, ALTITUDE_MIN, ALTITUDE_AVG,
|
||||
ALTITUDE_BASE, ASCENT_SECONDS, DESCENT_SECONDS, FLAT_SECONDS, ASCENT_DISTANCE,
|
||||
DESCENT_DISTANCE, FLAT_DISTANCE, ELEVATION_GAIN, ELEVATION_LOSS
|
||||
));
|
||||
put(GROUP_STROKES, Arrays.asList(
|
||||
STROKE_DISTANCE_AVG, STROKE_AVG_PER_SECOND, STROKES,
|
||||
STROKE_RATE_AVG, STROKE_RATE_MAX
|
||||
));
|
||||
put(GROUP_JUMPS, Arrays.asList(
|
||||
JUMPS, JUMP_RATE_AVG, JUMP_RATE_MAX
|
||||
));
|
||||
put(GROUP_SWIMMING, Arrays.asList(
|
||||
POOL_LENGTH, SWIM_AVG_CADENCE,
|
||||
SWOLF_INDEX, SWOLF_AVG, SWOLF_MAX, SWOLF_MIN, SWIM_STYLE
|
||||
));
|
||||
put(GROUP_CYCLING, Arrays.asList(
|
||||
LEFT_RIGHT_BALANCE,
|
||||
AVG_PEDAL_SMOOTHNESS, AVG_TORQUE_EFFECTIVENESS,
|
||||
AVG_LEFT_PCO, AVG_LEFT_POWER_PHASE, AVG_LEFT_POWER_PHASE_PEAK,
|
||||
AVG_RIGHT_PCO, AVG_RIGHT_POWER_PHASE, AVG_RIGHT_POWER_PHASE_PEAK,
|
||||
AVG_POWER_STANDING, MAX_POWER_STANDING,
|
||||
AVG_POWER_SEATING, MAX_POWER_SEATING,
|
||||
AVG_CADENCE_STANDING, AVG_CADENCE_SEATING,
|
||||
MAX_CADENCE_STANDING, MAX_CADENCE_SEATING,
|
||||
FRONT_GEAR_SHIFTS, REAR_GEAR_SHIFTS
|
||||
));
|
||||
put(GROUP_TRAINING_EFFECT, Arrays.asList(
|
||||
TRAINING_EFFECT_AEROBIC, TRAINING_EFFECT_ANAEROBIC, WORKOUT_LOAD,
|
||||
TRAINING_LOAD,
|
||||
INTENSITY_FACTOR, TRAINING_STRESS_SCORE,
|
||||
MAXIMUM_OXYGEN_UPTAKE, RECOVERY_TIME, LACTATE_THRESHOLD_HR
|
||||
));
|
||||
put(GROUP_LAPS, Arrays.asList(
|
||||
LAP_PACE_AVERAGE, LAPS, LANE_LENGTH
|
||||
));
|
||||
put(GROUP_PACE, Arrays.asList(
|
||||
));
|
||||
put(GROUP_RUNNING_FORM, Arrays.asList(
|
||||
GROUND_CONTACT_TIME_AVG, IMPACT_AVG, IMPACT_MAX, SWING_ANGLE_AVG,
|
||||
FORE_FOOT_LANDINGS, MID_FOOT_LANDINGS, BACK_FOOT_LANDINGS,
|
||||
EVERSION_ANGLE_AVG, EVERSION_ANGLE_MAX
|
||||
));
|
||||
put(GROUP_HEART_RATE_ZONES, Arrays.asList(
|
||||
HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_EASY, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC,
|
||||
HR_ZONE_THRESHOLD, HR_ZONE_EXTREME, HR_ZONE_MAXIMUM
|
||||
));
|
||||
put(GROUP_DIVING, Arrays.asList(
|
||||
AVG_DEPTH, MAX_DEPTH, START_CNS, END_CNS, START_N2, END_N2, DIVE_NUMBER, BOTTOM_TIME
|
||||
));
|
||||
put(SETS, Arrays.asList(
|
||||
));
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -16,6 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout;
|
||||
|
||||
public interface ActivitySummaryParser {
|
||||
/**
|
||||
@@ -37,10 +39,21 @@ public interface ActivitySummaryParser {
|
||||
* @param forDetails whether the parsing is for the details page. If this is false, the parser
|
||||
* should avoid slow operations such as reading and parsing raw files from
|
||||
* storage.
|
||||
* @return the update {@link BaseActivitySummary}
|
||||
* @return the {@link Workout}
|
||||
* @deprecated Use/implement {@link #parseWorkout(BaseActivitySummary, boolean)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails);
|
||||
|
||||
default Workout parseWorkout(BaseActivitySummary summary, final boolean forDetails) {
|
||||
final BaseActivitySummary baseActivitySummary = parseBinaryData(summary, forDetails);
|
||||
return new Workout(
|
||||
baseActivitySummary,
|
||||
ActivitySummaryData.fromJson(baseActivitySummary.getSummaryData()),
|
||||
Collections.emptyList()
|
||||
);
|
||||
}
|
||||
|
||||
static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
|
||||
final GBDevice gbDevice,
|
||||
final int timestampSeconds) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.workout
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData
|
||||
|
||||
data class Workout @JvmOverloads constructor(
|
||||
val summary: BaseActivitySummary,
|
||||
val data: ActivitySummaryData,
|
||||
val charts: List<WorkoutChart> = emptyList()
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.workout
|
||||
|
||||
import com.github.mikephil.charting.data.ChartData
|
||||
|
||||
data class WorkoutChart(
|
||||
val title: String,
|
||||
val chartData: ChartData<*>
|
||||
)
|
||||
-1
@@ -95,7 +95,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util
|
||||
|
||||
open class LiveDataEvent<out T>(private val content: T) {
|
||||
private var hasBeenHandled = false
|
||||
|
||||
/**
|
||||
* Returns the content if it hasn't been handled already.
|
||||
*/
|
||||
fun getContentIfNotHandled(): T? {
|
||||
return if (hasBeenHandled) {
|
||||
null
|
||||
} else {
|
||||
hasBeenHandled = true
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content, even if it's already been handled.
|
||||
*/
|
||||
fun peekContent(): T = content
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
/* Copyright (C) 2024-2025 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -18,12 +18,16 @@ package nodomain.freeyourgadget.gadgetbridge.util.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.mapsforge.core.graphics.Paint;
|
||||
import org.mapsforge.core.graphics.Style;
|
||||
import org.mapsforge.core.model.BoundingBox;
|
||||
import org.mapsforge.core.model.Dimension;
|
||||
import org.mapsforge.core.model.LatLong;
|
||||
import org.mapsforge.core.util.LatLongUtils;
|
||||
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
|
||||
import org.mapsforge.map.android.util.AndroidUtil;
|
||||
import org.mapsforge.map.android.view.MapView;
|
||||
@@ -39,9 +43,12 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
|
||||
public final class MapsManager {
|
||||
@@ -171,7 +178,22 @@ public final class MapsManager {
|
||||
isMapLoaded = false;
|
||||
}
|
||||
|
||||
public void setTrack(final List<LatLong> points) {
|
||||
public void setTrack(final List<? extends GPSCoordinate> trackPoints) {
|
||||
final Accumulator latitudeAccumulator = new Accumulator();
|
||||
final Accumulator longitudeAccumulator = new Accumulator();
|
||||
for (GPSCoordinate trackPoint : trackPoints) {
|
||||
latitudeAccumulator.add(trackPoint.getLatitude());
|
||||
longitudeAccumulator.add(trackPoint.getLongitude());
|
||||
}
|
||||
final double maxLat = latitudeAccumulator.getMax();
|
||||
final double minLat = latitudeAccumulator.getMin();
|
||||
final double maxLon = longitudeAccumulator.getMax();
|
||||
final double minLon = longitudeAccumulator.getMin();
|
||||
|
||||
final List<LatLong> points = trackPoints.stream()
|
||||
.map(p -> new LatLong(p.getLatitude(), p.getLongitude()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (polyline == null) {
|
||||
final Paint paint = AndroidGraphicFactory.INSTANCE.createPaint();
|
||||
final int trackColor = GBApplication.getPrefs().getInt(MapsManager.PREF_TRACK_COLOR, mContext.getResources().getColor(R.color.map_track_default));
|
||||
@@ -184,6 +206,15 @@ public final class MapsManager {
|
||||
}
|
||||
polyline.setPoints(points);
|
||||
mapView.getLayerManager().redrawLayers();
|
||||
|
||||
mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2));
|
||||
final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
|
||||
final byte zoom = LatLongUtils.zoomForBounds(
|
||||
new Dimension(displayMetrics.widthPixels, displayMetrics.heightPixels),
|
||||
new BoundingBox(minLat, minLon, maxLat, maxLon),
|
||||
mapView.getModel().displayModel.getTileSize()
|
||||
);
|
||||
mapView.setZoomLevel(zoom);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:orientation="vertical"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity">
|
||||
|
||||
<org.mapsforge.map.android.view.MapView
|
||||
android:id="@+id/activitygpsview"
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_summary_detail_scroll_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/activity_summary_detail_relative_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activityname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:scrollHorizontally="false"
|
||||
android:text="Activity Name"
|
||||
android:textSize="16sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activitydate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="Today, 10:00 - 12:00"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activityduration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="30min"
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top_layout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foregroundGravity="top"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_image"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/icon_placeholder"
|
||||
app:srcCompat="@drawable/ic_activity_unknown" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/summaryDetails"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/heart_rate"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/chartsFragmentHolder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gpsFragmentHolder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_spinner"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/agps_status_error"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,54 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/gps_track"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gpsWarning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/maps_not_found" />
|
||||
|
||||
<org.mapsforge.map.android.view.MapView
|
||||
android:id="@+id/activitygpsview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/unknown"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<com.github.mikephil.charting.charts.LineChart
|
||||
android:id="@+id/chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_spinner"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/agps_status_error"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activityname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:scrollHorizontally="false"
|
||||
android:text="Activity Name"
|
||||
android:textSize="16sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activitydate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="Today, 10:00 - 12:00"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activityduration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="30min"
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top_layout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foregroundGravity="top"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_image"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/icon_placeholder"
|
||||
app:srcCompat="@drawable/ic_activity_unknown" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- details -->
|
||||
<LinearLayout
|
||||
android:id="@+id/summaryDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!--
|
||||
Heart Rate chart
|
||||
TODO: Merge this into the dynamic charts
|
||||
-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/heart_rate"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/chartsFragmentHolder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Dynamic charts, provided by the device -->
|
||||
<LinearLayout
|
||||
android:id="@+id/dynamic_charts"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- Map for gpx track -->
|
||||
<FrameLayout
|
||||
android:id="@+id/gpsFragmentHolder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_spinner"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/agps_status_error"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/gps_track"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/row_separator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/missing_maps_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/maps_not_found"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="visible" />
|
||||
|
||||
<org.mapsforge.map.android.view.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_spinner"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/unknown_error"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -7,22 +7,12 @@
|
||||
android:icon="@drawable/ic_filter_alt"
|
||||
android:title="@string/pref_header_filter"
|
||||
app:iconTint="?attr/actionmenu_icon_color"
|
||||
app:showAsAction="always" />
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/overflowMenu"
|
||||
android:icon="@drawable/ic_menu"
|
||||
android:title="@string/menuitem_more"
|
||||
app:iconTint="?attr/actionmenu_icon_color"
|
||||
app:showAsAction="always">
|
||||
android:id="@+id/activity_action_manage_timestamp"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/reset_index"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/activity_action_manage_timestamp"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/reset_index"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<attr name="chart_not_worn" format="color" />
|
||||
<attr name="training_acute_load" format="color" />
|
||||
<attr name="alternate_row_background" format="color" />
|
||||
<attr name="sports_activity_summary_background" format="color" />
|
||||
|
||||
<attr name="deviceIconPrimary" format="color" />
|
||||
<attr name="deviceIconOnPrimary" format="color" />
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
<item name="training_acute_load">@color/training_load</item>
|
||||
<item name="alternate_row_background">@color/alternate_row_background_light</item>
|
||||
<item name="row_separator">@color/row_separator_light</item>
|
||||
<item name="sports_activity_summary_background">@color/chart_heartrate_fill</item>
|
||||
|
||||
<item name="deviceIconPrimary">#2196f3</item>
|
||||
<item name="deviceIconOnPrimary">#ffffff</item>
|
||||
@@ -79,7 +78,6 @@
|
||||
<item name="training_acute_load">@color/training_load_dark</item>
|
||||
<item name="alternate_row_background">@color/alternate_row_background_dark</item>
|
||||
<item name="row_separator">@color/row_separator_dark</item>
|
||||
<item name="sports_activity_summary_background">@color/alternate_row_background_dark</item>
|
||||
|
||||
<item name="deviceIconPrimary">#2196f3</item>
|
||||
<item name="deviceIconOnPrimary">#ffffff</item>
|
||||
@@ -110,7 +108,6 @@
|
||||
<item name="actionmenu_icon_color">?attr/colorOnSurface</item>
|
||||
<item name="alternate_row_background">?attr/colorSecondaryContainer</item>
|
||||
<item name="row_separator">@color/row_separator_light</item>
|
||||
<item name="sports_activity_summary_background">?attr/colorSurface</item>
|
||||
<item name="preferenceTheme">@style/AppPreferenceThemeOverlay</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<!-- FIXME: SDK 21 to 27 has light navbar on light background -->
|
||||
@@ -132,7 +129,6 @@
|
||||
<item name="actionmenu_icon_color">?attr/colorOnSurface</item>
|
||||
<item name="alternate_row_background">?attr/colorSecondaryContainer</item>
|
||||
<item name="row_separator">@color/row_separator_dark</item>
|
||||
<item name="sports_activity_summary_background">?attr/colorSurface</item>
|
||||
<item name="preferenceTheme">@style/AppPreferenceThemeOverlay</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
|
||||
|
||||
Reference in New Issue
Block a user