Delete Workouts, Recording Information, Automtic pause, GPX-library added for future export

This commit is contained in:
jannis 2019-08-18 15:53:58 +02:00
parent 7e406db592
commit c7ddf0ed58
28 changed files with 807 additions and 118 deletions

View File

@ -15,3 +15,20 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
<https://github.com/jenetics/jpx>
Copyright 2016-2019 Franz Wilhelmstötter
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -23,6 +23,7 @@ allprojects {
dependencies {
repositories {
jcenter()
maven { url 'https://jitpack.io' }
}
}
}
@ -44,6 +45,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
}
dependencies {
@ -59,6 +64,7 @@ dependencies {
implementation 'org.mapsforge:mapsforge-map-android:0.11.0'
implementation 'com.caverock:androidsvg:1.3'
implementation 'net.sf.kxml:kxml2:2.3.0'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation 'androidx.recyclerview:recyclerview:1.0.0'
testImplementation 'junit:junit:4.12'

BIN
app/libs/jpx-1.6.0.jar Normal file

Binary file not shown.

View File

@ -19,6 +19,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.tadris.fitness">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
@ -31,11 +32,16 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activity.ShowWorkoutActivity"></activity>
<activity android:name=".activity.RecordWorkoutActivity" />
<activity android:name=".activity.ListWorkoutsActivity" />
<activity android:name=".activity.LauncherActivity">
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".activity.ShowWorkoutActivity"
android:screenOrientation="portrait"/>
<activity android:name=".activity.RecordWorkoutActivity"
android:screenOrientation="portrait"/>
<activity android:name=".activity.ListWorkoutsActivity"
android:screenOrientation="portrait"/>
<activity android:name=".activity.LauncherActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -21,10 +21,7 @@ package de.tadris.fitness;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.location.LocationListener;
@ -44,17 +41,13 @@ public class Instance {
public AppDatabase db;
public LocationListener locationListener;
public UserPreferences userPreferences;
private Instance(Context context) {
db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.allowMainThreadQueries()
.addMigrations(new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE workout add topSpeed DOUBLE not null default 0.0");
}
})
.build();
locationListener= new LocationListener(context);
userPreferences= new UserPreferences();
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness;
public class UserPreferences {
/**
* Weight in kg
*/
public float weight= 80;
}

View File

@ -65,7 +65,7 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
@Override
public void onBindViewHolder(WorkoutViewHolder holder, final int position) {
holder.lengthText.setText(UnitUtils.getDistance(workouts[position].length));
holder.timeText.setText(UnitUtils.getHourMinuteTime(workouts[position].getDuration()));
holder.timeText.setText(UnitUtils.getHourMinuteTime(workouts[position].duration));
holder.root.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.activity;
import android.app.Activity;
import android.util.TypedValue;
public class FitoTrackActivity extends Activity {
protected int getThemePrimaryColor() {
final TypedValue value = new TypedValue ();
getTheme().resolveAttribute (android.R.attr.colorPrimary, value, true);
return value.data;
}
}

View File

@ -23,6 +23,7 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
public class LauncherActivity extends Activity {
@ -31,7 +32,21 @@ public class LauncherActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onResume(){
super.onResume();
init();
}
void init(){
Instance.getInstance(this);
start();
}
void start(){
startActivity(new Intent(this, ListWorkoutsActivity.class));
finish();
}
}

View File

@ -20,19 +20,27 @@
package de.tadris.fitness.activity;
import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.app.ActivityCompat;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.graphics.Style;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.download.TileDownloadLayer;
import org.mapsforge.map.layer.overlay.Polyline;
import java.util.ArrayList;
import java.util.List;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
@ -40,28 +48,96 @@ import de.tadris.fitness.data.Workout;
import de.tadris.fitness.location.LocationListener;
import de.tadris.fitness.location.WorkoutRecorder;
import de.tadris.fitness.map.MapManager;
import de.tadris.fitness.util.ThemeManager;
import de.tadris.fitness.util.UnitUtils;
public class RecordWorkoutActivity extends Activity implements LocationListener.LocationChangeListener {
public class RecordWorkoutActivity extends FitoTrackActivity implements LocationListener.LocationChangeListener {
public static String ACTIVITY= Workout.WORKOUT_TYPE_RUNNING;
MapView mapView;
TileDownloadLayer downloadLayer;
WorkoutRecorder recorder;
Polyline polyline;
List<LatLong> latLongList= new ArrayList<>();
InfoViewHolder[] infoViews= new InfoViewHolder[4];
TextView timeView, gpsStatusView;
boolean isResumed= false;
private Handler mHandler= new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(ThemeManager.getThemeByWorkoutType(ACTIVITY));
setContentView(R.layout.activity_record_workout);
this.mapView= new MapView(this);
downloadLayer= MapManager.setupMap(mapView);
setupMap();
((ViewGroup)findViewById(R.id.recordMapViewrRoot)).addView(mapView);
checkPermissions();
recorder= new WorkoutRecorder(this, Workout.WORKOUT_TYPE_RUNNING);
recorder= new WorkoutRecorder(this, ACTIVITY);
recorder.start();
infoViews[0]= new InfoViewHolder((TextView) findViewById(R.id.recordInfo1Title), (TextView) findViewById(R.id.recordInfo1Value));
infoViews[1]= new InfoViewHolder((TextView) findViewById(R.id.recordInfo2Title), (TextView) findViewById(R.id.recordInfo2Value));
infoViews[2]= new InfoViewHolder((TextView) findViewById(R.id.recordInfo3Title), (TextView) findViewById(R.id.recordInfo3Value));
infoViews[3]= new InfoViewHolder((TextView) findViewById(R.id.recordInfo4Title), (TextView) findViewById(R.id.recordInfo4Value));
timeView= findViewById(R.id.recordTime);
updateDescription();
startUpdater();
}
private void setupMap(){
this.mapView= new MapView(this);
downloadLayer= MapManager.setupMap(mapView);
}
private void updateLine(){
if(polyline != null){
mapView.getLayerManager().getLayers().remove(polyline);
}
Paint p= AndroidGraphicFactory.INSTANCE.createPaint();
p.setColor(getThemePrimaryColor());
p.setStrokeWidth(20);
p.setStyle(Style.STROKE);
polyline= new Polyline(p, AndroidGraphicFactory.INSTANCE);
polyline.setPoints(latLongList);
mapView.addLayer(polyline);
}
private void startUpdater(){
new Thread(new Runnable() {
@Override
public void run() {
try{
while (recorder.isActive()){
Thread.sleep(1000);
if(isResumed){
mHandler.post(new Runnable() {
@Override
public void run() {
updateDescription();
}
});
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}).start();
}
private void updateDescription(){
timeView.setText(UnitUtils.getHourMinuteSecondTime(recorder.getDuration()));
infoViews[0].setText(getString(R.string.workoutDistance), UnitUtils.getDistance(recorder.getDistance()));
infoViews[1].setText(getString(R.string.workoutBurnedEnergy), recorder.getCalories() + " kcal");
infoViews[2].setText(getString(R.string.workoutAvgSpeed), UnitUtils.getSpeed(recorder.getAvgSpeed()));
infoViews[3].setText(getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(recorder.getPauseDuration()));
}
private void stopAndSave(){
@ -92,7 +168,10 @@ public class RecordWorkoutActivity extends Activity implements LocationListener.
@Override
public void onLocationChange(Location location) {
mapView.getModel().mapViewPosition.animateTo(LocationListener.locationToLatLong(location));
LatLong latLong= LocationListener.locationToLatLong(location);
mapView.getModel().mapViewPosition.animateTo(latLong);
latLongList.add(latLong);
updateLine();
}
@Override
@ -108,12 +187,14 @@ public class RecordWorkoutActivity extends Activity implements LocationListener.
super.onPause();
downloadLayer.onPause();
Instance.getInstance(this).locationListener.unregisterLocationChangeListeners(this);
isResumed= false;
}
public void onResume(){
super.onResume();
downloadLayer.onResume();
Instance.getInstance(this).locationListener.registerLocationChangeListeners(this);
isResumed= true;
}
@Override
@ -133,5 +214,19 @@ public class RecordWorkoutActivity extends Activity implements LocationListener.
return super.onOptionsItemSelected(item);
}
public static class InfoViewHolder{
TextView titleView, valueView;
public InfoViewHolder(TextView titleView, TextView valueView) {
this.titleView = titleView;
this.valueView = valueView;
}
void setText(String title, String value){
this.titleView.setText(title);
this.valueView.setText(value);
}
}
}

View File

@ -19,8 +19,10 @@
package de.tadris.fitness.activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Handler;
@ -31,14 +33,25 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Description;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.highlight.Highlight;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.core.util.LatLongUtils;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.download.TileDownloadLayer;
import org.mapsforge.map.layer.overlay.FixedPixelCircle;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@ -46,13 +59,14 @@ import java.util.List;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutManager;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.map.MapManager;
import de.tadris.fitness.map.WorkoutLayer;
import de.tadris.fitness.util.ThemeManager;
import de.tadris.fitness.util.UnitUtils;
public class ShowWorkoutActivity extends Activity {
public class ShowWorkoutActivity extends FitoTrackActivity {
static Workout selectedWorkout;
List<WorkoutSample> samples;
@ -61,6 +75,7 @@ public class ShowWorkoutActivity extends Activity {
Resources.Theme theme;
MapView map;
TileDownloadLayer downloadLayer;
FixedPixelCircle highlightingCircle;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -68,7 +83,7 @@ public class ShowWorkoutActivity extends Activity {
workout= selectedWorkout;
samples= Arrays.asList(Instance.getInstance(this).db.workoutDao().getAllSamplesOfWorkout(workout.id));
setTheme(ThemeManager.getThemeByWorkout(workout, this));
setTheme(ThemeManager.getThemeByWorkout(workout));
setContentView(R.layout.activity_show_workout);
getActionBar().setDisplayHomeAsUpEnabled(true);
@ -77,28 +92,31 @@ public class ShowWorkoutActivity extends Activity {
root= findViewById(R.id.showWorkoutRoot);
addTitle("Zeit");
addKeyValue("Datum", getDate(), "Dauer", UnitUtils.getHourMinuteTime(workout.getDuration()));
addKeyValue("Startzeit", SimpleDateFormat.getTimeInstance().format(new Date(workout.start)),
"Endzeit", SimpleDateFormat.getTimeInstance().format(new Date(workout.end)));
addTitle(getString(R.string.workoutTime));
addKeyValue(getString(R.string.workoutDate), getDate());
addKeyValue(getString(R.string.workoutDuration), UnitUtils.getHourMinuteSecondTime(workout.duration),
getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(workout.pauseDuration));
addKeyValue(getString(R.string.workoutStartTime), SimpleDateFormat.getTimeInstance().format(new Date(workout.start)),
getString(R.string.workoutEndTime), SimpleDateFormat.getTimeInstance().format(new Date(workout.end)));
addKeyValue("Distanz", UnitUtils.getDistance(workout.length), "Pace", UnitUtils.round(workout.avgPace, 1) + " min/km");
addKeyValue(getString(R.string.workoutDistance), UnitUtils.getDistance(workout.length), getString(R.string.workoutPace), UnitUtils.getPace(workout.avgPace));
addTitle("Geschwindigkeit");
addKeyValue("Durchschnittsgeschw.", UnitUtils.getSpeed(workout.avgSpeed),
"Top Geschw.", UnitUtils.round(workout.topSpeed, 1) + " km/h");
// TODO: add speed diagram
addTitle("Verbrauchte Energie");
addKeyValue("Gesamtverbrauch", workout.calorie + " kcal",
"Relativverbrauch", UnitUtils.round(((double)workout.calorie / workout.length / 1000), 2) + " kcal/km");
addTitle("Route");
addTitle(getString(R.string.workoutRoute));
addMap();
addTitle(getString(R.string.workoutSpeed));
addKeyValue(getString(R.string.workoutAvgSpeed), UnitUtils.getSpeed(workout.avgSpeed),
getString(R.string.workoutTopSpeed), UnitUtils.getSpeed(workout.topSpeed));
addSpeedDiagram();
addTitle(getString(R.string.workoutBurnedEnergy));
addKeyValue(getString(R.string.workoutTotalEnergy), workout.calorie + " kcal",
getString(R.string.workoutEnergyConsumption), UnitUtils.getPace((double)workout.calorie / workout.length / 1000));
}
String getDate(){
@ -113,6 +131,7 @@ public class ShowWorkoutActivity extends Activity {
textView.setTextColor(getThemePrimaryColor());
textView.setTypeface(Typeface.DEFAULT_BOLD);
textView.setAllCaps(true);
textView.setPadding(0, 20, 0, 0);
root.addView(textView);
}
@ -137,8 +156,62 @@ public class ShowWorkoutActivity extends Activity {
root.addView(v);
}
void addDiagram(){
void addSpeedDiagram(){
LineChart chart= new LineChart(this);
WorkoutManager.roundSpeedValues(samples);
List<Entry> entries = new ArrayList<>();
for (WorkoutSample sample : samples) {
// turn your data into Entry objects
Entry e= new Entry((float)(sample.relativeTime) / 1000f / 60f, (float)sample.tmpRoundedSpeed*3.6f);
entries.add(e);
sample.tmpEntry= e;
}
LineDataSet dataSet = new LineDataSet(entries, "Speed"); // add entries to dataset // TODO: localisatoin
dataSet.setColor(getThemePrimaryColor());
dataSet.setValueTextColor(getThemePrimaryColor());
dataSet.setDrawCircles(false);
dataSet.setLineWidth(4);
dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
Description description= new Description();
description.setText("min - km/h");
LineData lineData = new LineData(dataSet);
chart.setData(lineData);
chart.setScaleEnabled(false);
chart.setDescription(description);
chart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() {
@Override
public void onValueSelected(Entry e, Highlight h) {
onNothingSelected();
Paint p= AndroidGraphicFactory.INSTANCE.createPaint();
p.setColor(Color.BLUE);
highlightingCircle= new FixedPixelCircle(getSamplebyTime(e).toLatLong(), 10, p, null);
map.addLayer(highlightingCircle);
}
@Override
public void onNothingSelected() {
if(highlightingCircle != null){
map.getLayerManager().getLayers().remove(highlightingCircle);
}
}
});
chart.invalidate();
root.addView(chart, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getWindowManager().getDefaultDisplay().getWidth()*3/4));
}
WorkoutSample getSamplebyTime(Entry entry){
for(WorkoutSample sample : samples){
if(sample.tmpEntry.equalTo(entry)){
return sample;
}
}
return null;
}
void addMap(){
@ -165,12 +238,14 @@ public class ShowWorkoutActivity extends Activity {
root.addView(map, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getWindowManager().getDefaultDisplay().getWidth()*3/4));
map.setAlpha(0);
}
private int getThemePrimaryColor() {
final TypedValue value = new TypedValue ();
getTheme().resolveAttribute (android.R.attr.colorPrimary, value, true);
return value.data;
Paint pGreen= AndroidGraphicFactory.INSTANCE.createPaint();
pGreen.setColor(Color.GREEN);
map.addLayer(new FixedPixelCircle(samples.get(0).toLatLong(), 20, pGreen, null));
Paint pRed= AndroidGraphicFactory.INSTANCE.createPaint();
pRed.setColor(Color.RED);
map.addLayer(new FixedPixelCircle(samples.get(samples.size()-1).toLatLong(), 20, pRed, null));
}
@Override
@ -198,11 +273,29 @@ public class ShowWorkoutActivity extends Activity {
return true;
}
private void deleteWorkout(){
Instance.getInstance(this).db.workoutDao().deleteWorkout(workout);
finish();
}
private void showDeleteDialog(){
new AlertDialog.Builder(this).setTitle(R.string.deleteWorkout)
.setMessage(R.string.deleteWorkoutMessage)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteWorkout();
}
})
.create().show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if(id == R.id.actionDeleteWorkout){
// TODO: delete workout
showDeleteDialog();
return true;
}else if(id == android.R.id.home){
finish();
@ -211,18 +304,4 @@ public class ShowWorkoutActivity extends Activity {
return super.onOptionsItemSelected(item);
}
/*private int zoomToBounds(BoundingBox boundingBox) {
int TILE_SIZE= map.getModel().displayModel.getTileSize();
Dimension mapViewDimension = map.getModel().mapViewDimension.getDimension();
if(mapViewDimension == null)
return 0;
double dxMax = longitudeToPixelX(boundingBox.maxLongitude, (byte) 0) / TILE_SIZE;
double dxMin = longitudeToPixelX(boundingBox.minLongitude, (byte) 0) / TILE_SIZE;
double zoomX = floor(-log(3.8) * log(abs(dxMax-dxMin)) + mapViewDimension.width / TILE_SIZE);
double dyMax = latitudeToPixelY(boundingBox.maxLatitude, (byte) 0) / TILE_SIZE;
double dyMin = latitudeToPixelY(boundingBox.minLatitude, (byte) 0) / TILE_SIZE;
double zoomY = floor(-log(3.8) * log(abs(dyMax-dyMin)) + mapViewDimension.height / TILE_SIZE);
return Double.valueOf(min(zoomX, zoomY)).intValue();
}*/
}

View File

@ -22,7 +22,7 @@ package de.tadris.fitness.data;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(version = 3, entities = {Workout.class, WorkoutSample.class})
@Database(version = 1, entities = {Workout.class, WorkoutSample.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract WorkoutDao workoutDao();
}

View File

@ -36,6 +36,10 @@ public class Workout{
public long start;
public long end;
public long duration;
public long pauseDuration;
/**
* Length of workout in meters
*/
@ -60,9 +64,5 @@ public class Workout{
public int calorie;
public long getDuration(){
return end - start;
}
}

View File

@ -20,8 +20,10 @@
package de.tadris.fitness.data;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@Dao
public interface WorkoutDao {
@ -29,7 +31,7 @@ public interface WorkoutDao {
@Query("SELECT * FROM workout_sample WHERE workout_id = :workout_id")
WorkoutSample[] getAllSamplesOfWorkout(long workout_id);
@Query("SELECT * FROM workout")
@Query("SELECT * FROM workout ORDER BY start DESC")
Workout[] getWorkouts();
@Insert
@ -38,7 +40,11 @@ public interface WorkoutDao {
@Insert
void insertWorkout(Workout workout);
@Query("SELECT * FROM workout ORDER BY start DESC LIMIT 1")
Workout findLastWorkout();
@Delete
void deleteWorkout(Workout workout);
@Update
void updateWorkout(Workout workout);
}

View File

@ -37,13 +37,15 @@ public class WorkoutManager {
// Calculating values
double length= 0;
for(int i= 1; i < samples.size(); i++){
length+= samples.get(i - 1).toLatLong().sphericalDistance(samples.get(i).toLatLong());
double sampleLength= samples.get(i - 1).toLatLong().sphericalDistance(samples.get(i).toLatLong());
long timeDiff= (samples.get(i).relativeTime - samples.get(i - 1).relativeTime) / 1000;
length+= sampleLength;
samples.get(i).speed= Math.abs(sampleLength / timeDiff);
}
workout.length= (int)length;
workout.avgSpeed= ((double) workout.length) / ((double) workout.getDuration() / 1000);
workout.avgPace= (double)(workout.getDuration() / 1000 / 60) / ((double) workout.length / 1000);
workout.calorie= CalorieCalculator.calculateCalories(workout, 80);
// TODO: use user weight
workout.avgSpeed= ((double) workout.length) / ((double) workout.duration / 1000);
workout.avgPace= ((double)workout.duration / 1000 / 60) / ((double) workout.length / 1000);
workout.calorie= CalorieCalculator.calculateCalories(workout, Instance.getInstance(context).userPreferences.weight);
// Setting workoutId in the samples
int i= 0;
@ -65,4 +67,17 @@ public class WorkoutManager {
}
public static void roundSpeedValues(List<WorkoutSample> samples){
for(int i= 0; i < samples.size(); i++){
WorkoutSample sample= samples.get(i);
if(i == 0){
sample.tmpRoundedSpeed= (sample.speed+samples.get(i+1).speed) / 2;
}else if(i == samples.size()-1){
sample.tmpRoundedSpeed= (sample.speed+samples.get(i-1).speed) / 2;
}else{
sample.tmpRoundedSpeed= (sample.speed+samples.get(i-1).speed+samples.get(i+1).speed) / 3;
}
}
}
}

View File

@ -22,8 +22,11 @@ package de.tadris.fitness.data;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.github.mikephil.charting.data.Entry;
import org.mapsforge.core.model.LatLong;
import static androidx.room.ForeignKey.CASCADE;
@ -42,14 +45,26 @@ public class WorkoutSample{
@ColumnInfo(name = "workout_id")
public long workoutId;
public long time;
public long absoluteTime;
public long relativeTime;
public double lat;
public double lon;
public double elevation;
public double relativeElevation;
public double speed;
@Ignore
public Entry tmpEntry;
@Ignore
public double tmpRoundedSpeed;
public LatLong toLatLong(){
return new LatLong(lat, lon);
}

View File

@ -124,7 +124,7 @@ public class LocationListener implements android.location.LocationListener {
boolean result = false;
for (String provider : this.locationManager.getProviders(true)) {
if (LocationManager.GPS_PROVIDER.equals(provider) || LocationManager.NETWORK_PROVIDER.equals(provider)) {
if (LocationManager.GPS_PROVIDER.equals(provider)) {
result = true;
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;

View File

@ -32,17 +32,33 @@ import de.tadris.fitness.Instance;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutManager;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.util.CalorieCalculator;
public class WorkoutRecorder implements LocationListener.LocationChangeListener {
private static final int MIN_DISTANCE= 5;
private static int getMinDistance(String workoutType){
switch (workoutType){
case Workout.WORKOUT_TYPE_HIKING:
case Workout.WORKOUT_TYPE_RUNNING:
return 8;
case Workout.WORKOUT_TYPE_CYCLING:
return 15;
default: return 10;
}
}
private static final int PAUSE_TIME= 10000;
private Context context;
private Workout workout;
private RecordingState state;
private List<WorkoutSample> samples= new ArrayList<>();
private final List<WorkoutSample> samples= new ArrayList<>();
private long time= 0;
private long pauseTime= 0;
private long lastResume;
private long lastPause= 0;
private long lastSampleTime= 0;
private double distance= 0;
public WorkoutRecorder(Context context, String workoutType) {
this.context= context;
@ -54,9 +70,11 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
public void start(){
if(state == RecordingState.IDLE){
Log.i("Recorder", "");
Log.i("Recorder", "Start");
workout.start= System.currentTimeMillis();
resume();
Instance.getInstance(context).locationListener.registerLocationChangeListeners(this);
startWatchdog();
}else if(state == RecordingState.PAUSED){
resume();
}else if(state != RecordingState.RUNNING){
@ -64,53 +82,161 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
}
}
public boolean isActive(){
return state == RecordingState.RUNNING || state == RecordingState.PAUSED;
}
private void startWatchdog(){
new Thread(new Runnable() {
@Override
public void run() {
try {
while (isActive()){
synchronized (samples){
if(samples.size() > 2){
WorkoutSample lastSample= samples.get(samples.size()-1);
if(System.currentTimeMillis() - lastSampleTime > PAUSE_TIME){
if(state == RecordingState.RUNNING){
pause();
}
}else{
if(state == RecordingState.PAUSED){
resume();
}
}
}
}
Thread.sleep(5000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
private void resume(){
Log.i("Recorder", "Resume");
state= RecordingState.RUNNING;
lastResume= System.currentTimeMillis();
Instance.getInstance(context).locationListener.registerLocationChangeListeners(this);
if(lastPause != 0){
pauseTime+= System.currentTimeMillis() - lastPause;
}
}
public void pause(){
if(state == RecordingState.RUNNING){
Log.i("Recorder", "Pause");
state= RecordingState.PAUSED;
time+= System.currentTimeMillis() - lastResume;
Instance.getInstance(context).locationListener.unregisterLocationChangeListeners(this);
lastPause= System.currentTimeMillis();
}
}
public void stop(){
Log.i("Recorder", "Stop");
if(state == RecordingState.PAUSED){
resume();
}
pause();
workout.end= System.currentTimeMillis();
workout.duration= time;
workout.pauseDuration= pauseTime;
state= RecordingState.STOPPED;
Instance.getInstance(context).locationListener.unregisterLocationChangeListeners(this);
}
public void save(){
if(state != RecordingState.STOPPED){
throw new IllegalStateException("Cannot save recording, recorder was not stopped. state = " + state);
}
Log.i("Recorder", "Save");
synchronized (samples){
WorkoutManager.insertWorkout(context, workout, samples);
}
}
public int getSampleCount(){
synchronized (samples){
return samples.size();
}
}
@Override
public void onLocationChange(Location location) {
if(state == RecordingState.RUNNING){
if(isActive()){
double distance= 0;
if(getSampleCount() > 0){
synchronized (samples){
WorkoutSample lastSample= samples.get(samples.size() - 1);
if(LocationListener.locationToLatLong(location).sphericalDistance(new LatLong(lastSample.lat, lastSample.lon)) < MIN_DISTANCE){
distance= LocationListener.locationToLatLong(location).sphericalDistance(new LatLong(lastSample.lat, lastSample.lon));
long timediff= lastSample.absoluteTime - location.getTime();
if(distance < getMinDistance(workout.workoutType) && timediff < 500){
return;
}
}
}
lastSampleTime= System.currentTimeMillis();
if(state == RecordingState.RUNNING && location.getTime() > workout.start){
if(samples.size() == 2){
lastResume= System.currentTimeMillis();
workout.start= System.currentTimeMillis();
lastPause= 0;
time= 0;
pauseTime= 0;
this.distance= 0;
}
this.distance+= distance;
WorkoutSample sample= new WorkoutSample();
sample.lat= location.getLatitude();
sample.lon= location.getLongitude();
sample.elevation= location.getAltitude();
sample.relativeElevation= 0.0;
sample.speed= location.getSpeed();
sample.time= location.getTime();
sample.relativeTime= location.getTime() - workout.start - pauseTime;
sample.absoluteTime= location.getTime();
synchronized (samples){
samples.add(sample);
}
}
}
}
/**
* Returns the distance in meters
*/
public int getDistance(){
return (int)distance;
}
public int getCalories(){
workout.avgSpeed= getAvgSpeed();
return CalorieCalculator.calculateCalories(workout, Instance.getInstance(context).userPreferences.weight);
}
/**
*
* @return avgSpeed in m/s
*/
public double getAvgSpeed(){
return distance / (double)(getDuration() / 1000);
}
public long getPauseDuration(){
if(state == RecordingState.PAUSED){
return pauseTime + (System.currentTimeMillis() - lastPause);
}else{
return pauseTime;
}
}
public long getDuration(){
if(state == RecordingState.RUNNING){
return time + (System.currentTimeMillis() - lastResume);
}else{
return time;
}
}

View File

@ -30,13 +30,15 @@ public class CalorieCalculator {
* @return calories burned
*/
public static int calculateCalories(Workout workout, double weight){
int mins= (int)(workout.getDuration() / 1000 / 60);
double mins= (double)(workout.duration / 1000) / 60;
return (int)(mins * (getMET(workout) * 3.5 * weight) / 200);
}
/**
* calorie calculation based on @link { https://www.topendsports.com/weight-loss/energy-met.htm }
*
* workoutType and avgSpeed of workout have to be set
*
* @param workout
* @return MET
*/

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.util;
import android.content.Context;
import de.tadris.fitness.Instance;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
import io.jenetics.jpx.GPX;
import io.jenetics.jpx.Track;
import io.jenetics.jpx.TrackSegment;
public class GpxExporter {
public static void exportWorkout(Context context, Workout workout){
GPX.Builder builder= GPX.builder();
builder.addTrack(toTrack(context, workout));
}
public static Track toTrack(Context context, Workout workout){
Track.Builder track= Track.builder();
TrackSegment.Builder segment= TrackSegment.builder();
WorkoutSample[] samples= Instance.getInstance(context).db.workoutDao().getAllSamplesOfWorkout(workout.id);
for(WorkoutSample sample : samples){
segment.addPoint(p -> p.lat(sample.lat).lon(sample.lon).ele(sample.elevation).speed(sample.speed).time(sample.absoluteTime));
}
track.addSegment(segment.build());
track.src("FitoTrack");
track.type(workout.workoutType);
return track.build();
}
}

View File

@ -19,21 +19,22 @@
package de.tadris.fitness.util;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
public class ThemeManager {
public static int getThemeByWorkout(Workout workout, Context context){
switch (workout.workoutType){
public static int getThemeByWorkoutType(String type){
switch (type){
case Workout.WORKOUT_TYPE_RUNNING: return R.style.Running;
case Workout.WORKOUT_TYPE_CYCLING: return R.style.Bicycling;
default: return R.style.AppTheme;
}
}
public static int getThemeByWorkout(Workout workout){
return getThemeByWorkoutType(workout.workoutType);
}
}

View File

@ -34,13 +34,44 @@ public class UnitUtils {
}
}
public static String getHourMinuteSecondTime(long time){
long totalSeks= time / 1000;
long totalMins= totalSeks / 60;
long hours= totalMins / 60;
long mins= totalMins % 60;
long seks= totalSeks % 60;
String minStr= (mins < 10 ? "0" : "") + mins;
String sekStr= (seks < 10 ? "0" : "") + seks;
return hours + ":" + minStr + ":" + sekStr;
}
/**
*
* @param pace Pace in min/km
* @return Pace
*/
public static String getPace(double pace){
// TODO: use preferred unit chosen by user
return round(pace, 1) + " min/km";
}
/**
*
* @param consumption consumption in kcal/km
* @return
*/
public static String getRelativeEnergyConsumption(double consumption){
// TODO: use preferred unit chosen by user
return round(consumption, 2) + " kcal/km";
}
/**
*
* @param distance Distance in meters
* @return String in preferred unit
*/
public static String getDistance(int distance){
// TODO: use preferred unit by user
// TODO: use preferred unit chosen by user
if(distance >= 1000){
return getDistanceInKilometers((double)distance);
}else{
@ -54,7 +85,7 @@ public class UnitUtils {
* @return speed in km/h
*/
public static String getSpeed(double speed){
// TODO: use preferred unit by user
// TODO: use preferred unit chosen by user
return round(speed*3.6, 1) + " km/h";
}

View File

@ -18,8 +18,17 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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=".activity.LauncherActivity" />
android:theme="@style/AppThemeNoActionbar"
tools:context=".activity.LauncherActivity">
<ImageView
android:id="@+id/imageView2"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:src="@mipmap/ic_launcher" />
</FrameLayout>

View File

@ -19,6 +19,7 @@
-->
<RelativeLayout 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"
@ -30,13 +31,6 @@
android:layout_above="@id/recordInfoRoot">
<ImageView
android:id="@+id/imageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/location_marker" />
<LinearLayout
android:id="@+id/recordMapViewrRoot"
android:layout_width="match_parent"
@ -46,6 +40,14 @@
</LinearLayout>
<ImageView
android:id="@+id/imageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/location_marker" />
</FrameLayout>
<LinearLayout
@ -58,11 +60,132 @@
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:id="@+id/recordTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="TextView" />
android:fontFamily="sans-serif-black"
android:text="0:44:08"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="30sp"
android:textStyle="bold" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/recordInfo1Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/workoutDistance"
android:textAlignment="center"
android:textAllCaps="true"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo1Value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="2,06 km"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo2Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/workoutBurnedEnergy"
android:textAlignment="center"
android:textAllCaps="true"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo2Value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="30 kcal"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/linearLayout"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/recordInfo3Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/workoutAvgSpeed"
android:textAlignment="center"
android:textAllCaps="true"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo3Value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="7 km/h"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo4Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/workoutBurnedEnergy"
android:textAlignment="center"
android:textAllCaps="true"
android:textStyle="bold" />
<TextView
android:id="@+id/recordInfo4Value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="30 kcal"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -98,6 +98,6 @@
android:layout_marginEnd="207dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintGuide_begin="204dp"
app:layout_constraintGuide_percent="0.5"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

View File

@ -23,5 +23,5 @@
<item
android:id="@+id/actionDeleteWorkout"
android:showAsAction="ifRoom"
android:title="@string/deleteWorkout" />
android:title="@string/delete" />
</menu>

View File

@ -21,5 +21,26 @@
<string name="app_name">FitoTrack</string>
<string name="workout_add">Add</string>
<string name="workoutStopRecording">Stop</string>
<string name="deleteWorkout">Delete</string>
<string name="delete">Delete</string>
<string name="workoutTime">Time</string>
<string name="workoutDate">Date</string>
<string name="workoutDuration">Duration</string>
<string name="workoutPauseDuration">Pause Duration</string>
<string name="workoutStartTime">Start Time</string>
<string name="workoutEndTime">End Time</string>
<string name="workoutDistance">Distance</string>
<string name="workoutPace">Pace</string>
<string name="workoutRoute">Route</string>
<string name="workoutSpeed">Speed</string>
<string name="workoutAvgSpeed">Avg. Speed</string>
<string name="workoutTopSpeed">Top Speed</string>
<string name="workoutBurnedEnergy">Burned Energy</string>
<string name="workoutTotalEnergy">Total Energy</string>
<string name="workoutEnergyConsumption">Energy Consumption</string>
<string name="deleteWorkout">Delete Workout</string>
<string name="deleteWorkoutMessage">Do you really want to delete the workout?</string>
<string name="cancel">Cancel</string>
</resources>

View File

@ -19,6 +19,14 @@
<resources>
<style name="AppThemeNoActionbar" parent="android:Theme.Material.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:colorPrimary">@color/colorPrimary</item>
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:colorAccent">@color/colorAccent</item>
</style>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<!-- Customize your theme here. -->