diff --git a/NOTICE.md b/NOTICE.md index c6cba5a..b272c77 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -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. + + + + + 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. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 86b19a8..b6f37d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/libs/jpx-1.6.0.jar b/app/libs/jpx-1.6.0.jar new file mode 100644 index 0000000..d716475 Binary files /dev/null and b/app/libs/jpx-1.6.0.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81062b7..b12fa9b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ --> @@ -31,11 +32,16 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> - - - - + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + + + + diff --git a/app/src/main/java/de/tadris/fitness/Instance.java b/app/src/main/java/de/tadris/fitness/Instance.java index 25c8edd..517a295 100644 --- a/app/src/main/java/de/tadris/fitness/Instance.java +++ b/app/src/main/java/de/tadris/fitness/Instance.java @@ -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(); } } diff --git a/app/src/main/java/de/tadris/fitness/UserPreferences.java b/app/src/main/java/de/tadris/fitness/UserPreferences.java new file mode 100644 index 0000000..968607d --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/UserPreferences.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Jannis Scheibe + * + * 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 . + */ + +package de.tadris.fitness; + +public class UserPreferences { + + /** + * Weight in kg + */ + public float weight= 80; + +} diff --git a/app/src/main/java/de/tadris/fitness/WorkoutAdapter.java b/app/src/main/java/de/tadris/fitness/WorkoutAdapter.java index 0d96d45..851e6c0 100644 --- a/app/src/main/java/de/tadris/fitness/WorkoutAdapter.java +++ b/app/src/main/java/de/tadris/fitness/WorkoutAdapter.java @@ -65,7 +65,7 @@ public class WorkoutAdapter extends RecyclerView.Adapter + * + * 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 . + */ + +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; + } + + +} diff --git a/app/src/main/java/de/tadris/fitness/activity/LauncherActivity.java b/app/src/main/java/de/tadris/fitness/activity/LauncherActivity.java index f14f0cd..d331807 100644 --- a/app/src/main/java/de/tadris/fitness/activity/LauncherActivity.java +++ b/app/src/main/java/de/tadris/fitness/activity/LauncherActivity.java @@ -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(); } } diff --git a/app/src/main/java/de/tadris/fitness/activity/RecordWorkoutActivity.java b/app/src/main/java/de/tadris/fitness/activity/RecordWorkoutActivity.java index 7e4cd49..ab5f52c 100644 --- a/app/src/main/java/de/tadris/fitness/activity/RecordWorkoutActivity.java +++ b/app/src/main/java/de/tadris/fitness/activity/RecordWorkoutActivity.java @@ -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 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); + } + } + } diff --git a/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java b/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java index d121a47..fbf8267 100644 --- a/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java +++ b/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java @@ -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 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 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(); - }*/ - } diff --git a/app/src/main/java/de/tadris/fitness/data/AppDatabase.java b/app/src/main/java/de/tadris/fitness/data/AppDatabase.java index e1ce9ed..cd1fc75 100644 --- a/app/src/main/java/de/tadris/fitness/data/AppDatabase.java +++ b/app/src/main/java/de/tadris/fitness/data/AppDatabase.java @@ -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(); } diff --git a/app/src/main/java/de/tadris/fitness/data/Workout.java b/app/src/main/java/de/tadris/fitness/data/Workout.java index 4c87a23..69937fc 100644 --- a/app/src/main/java/de/tadris/fitness/data/Workout.java +++ b/app/src/main/java/de/tadris/fitness/data/Workout.java @@ -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; - } - } \ No newline at end of file diff --git a/app/src/main/java/de/tadris/fitness/data/WorkoutDao.java b/app/src/main/java/de/tadris/fitness/data/WorkoutDao.java index 7efa777..eeea868 100644 --- a/app/src/main/java/de/tadris/fitness/data/WorkoutDao.java +++ b/app/src/main/java/de/tadris/fitness/data/WorkoutDao.java @@ -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); + } diff --git a/app/src/main/java/de/tadris/fitness/data/WorkoutManager.java b/app/src/main/java/de/tadris/fitness/data/WorkoutManager.java index 1f1e83f..810bdc2 100644 --- a/app/src/main/java/de/tadris/fitness/data/WorkoutManager.java +++ b/app/src/main/java/de/tadris/fitness/data/WorkoutManager.java @@ -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 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; + } + } + } + } diff --git a/app/src/main/java/de/tadris/fitness/data/WorkoutSample.java b/app/src/main/java/de/tadris/fitness/data/WorkoutSample.java index 01117c3..5736058 100644 --- a/app/src/main/java/de/tadris/fitness/data/WorkoutSample.java +++ b/app/src/main/java/de/tadris/fitness/data/WorkoutSample.java @@ -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); } diff --git a/app/src/main/java/de/tadris/fitness/location/LocationListener.java b/app/src/main/java/de/tadris/fitness/location/LocationListener.java index d04769f..e7ad0cb 100644 --- a/app/src/main/java/de/tadris/fitness/location/LocationListener.java +++ b/app/src/main/java/de/tadris/fitness/location/LocationListener.java @@ -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; diff --git a/app/src/main/java/de/tadris/fitness/location/WorkoutRecorder.java b/app/src/main/java/de/tadris/fitness/location/WorkoutRecorder.java index 8822822..6a9dc09 100644 --- a/app/src/main/java/de/tadris/fitness/location/WorkoutRecorder.java +++ b/app/src/main/java/de/tadris/fitness/location/WorkoutRecorder.java @@ -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 samples= new ArrayList<>(); + private final List 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,52 +82,160 @@ 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); } - WorkoutManager.insertWorkout(context, workout, samples); + Log.i("Recorder", "Save"); + synchronized (samples){ + WorkoutManager.insertWorkout(context, workout, samples); + } } public int getSampleCount(){ - return samples.size(); + synchronized (samples){ + return samples.size(); + } } @Override public void onLocationChange(Location location) { - if(state == RecordingState.RUNNING){ + if(isActive()){ + double distance= 0; if(getSampleCount() > 0){ - WorkoutSample lastSample= samples.get(samples.size() - 1); - if(LocationListener.locationToLatLong(location).sphericalDistance(new LatLong(lastSample.lat, lastSample.lon)) < MIN_DISTANCE){ - return; + synchronized (samples){ + WorkoutSample lastSample= samples.get(samples.size() - 1); + 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; + } } } - WorkoutSample sample= new WorkoutSample(); - sample.lat= location.getLatitude(); - sample.lon= location.getLongitude(); - sample.speed= location.getSpeed(); - sample.time= location.getTime(); - samples.add(sample); + 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.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; } } diff --git a/app/src/main/java/de/tadris/fitness/util/CalorieCalculator.java b/app/src/main/java/de/tadris/fitness/util/CalorieCalculator.java index 5f50d92..ae3ccc7 100644 --- a/app/src/main/java/de/tadris/fitness/util/CalorieCalculator.java +++ b/app/src/main/java/de/tadris/fitness/util/CalorieCalculator.java @@ -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 */ diff --git a/app/src/main/java/de/tadris/fitness/util/GpxExporter.java b/app/src/main/java/de/tadris/fitness/util/GpxExporter.java new file mode 100644 index 0000000..272f0b7 --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/util/GpxExporter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019 Jannis Scheibe + * + * 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 . + */ + +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(); + } + +} diff --git a/app/src/main/java/de/tadris/fitness/util/ThemeManager.java b/app/src/main/java/de/tadris/fitness/util/ThemeManager.java index 5510c22..82e8188 100644 --- a/app/src/main/java/de/tadris/fitness/util/ThemeManager.java +++ b/app/src/main/java/de/tadris/fitness/util/ThemeManager.java @@ -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); + } + } diff --git a/app/src/main/java/de/tadris/fitness/util/UnitUtils.java b/app/src/main/java/de/tadris/fitness/util/UnitUtils.java index d8a5dad..7c06993 100644 --- a/app/src/main/java/de/tadris/fitness/util/UnitUtils.java +++ b/app/src/main/java/de/tadris/fitness/util/UnitUtils.java @@ -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"; } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a860e62..4b9fda9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,8 +18,17 @@ ~ along with this program. If not, see . --> - \ No newline at end of file + android:theme="@style/AppThemeNoActionbar" + tools:context=".activity.LauncherActivity"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_record_workout.xml b/app/src/main/res/layout/activity_record_workout.xml index b581b3c..f0afe2e 100644 --- a/app/src/main/res/layout/activity_record_workout.xml +++ b/app/src/main/res/layout/activity_record_workout.xml @@ -19,6 +19,7 @@ --> - - + + + + android:fontFamily="sans-serif-black" + android:text="0:44:08" + android:textAlignment="center" + android:textColor="@android:color/black" + android:textSize="30sp" + android:textStyle="bold" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show_entry.xml b/app/src/main/res/layout/show_entry.xml index 8076af6..a59161c 100644 --- a/app/src/main/res/layout/show_entry.xml +++ b/app/src/main/res/layout/show_entry.xml @@ -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" /> \ No newline at end of file diff --git a/app/src/main/res/menu/show_workout_menu.xml b/app/src/main/res/menu/show_workout_menu.xml index da4a8a4..37e7ed1 100644 --- a/app/src/main/res/menu/show_workout_menu.xml +++ b/app/src/main/res/menu/show_workout_menu.xml @@ -23,5 +23,5 @@ + android:title="@string/delete" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21d86c7..1ad9eef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,5 +21,26 @@ FitoTrack Add Stop - Delete + Delete + + Time + Date + Duration + Pause Duration + Start Time + End Time + Distance + Pace + Route + Speed + Avg. Speed + Top Speed + Burned Energy + Total Energy + Energy Consumption + + Delete Workout + Do you really want to delete the workout? + + Cancel diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4b3749b..a56d984 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,6 +19,14 @@ + +