Compare commits

...

117 Commits
v2.0 ... master

Author SHA1 Message Date
jannis
be3ce4f1c3 updated Readme
removed link to the Roadmap
2020-03-17 15:39:36 +01:00
jannis
d372e5ffff updated changelogs 2020-03-17 15:34:19 +01:00
jannis
428256294a Merge branch 'master' into 5.0 2020-03-17 15:32:18 +01:00
jannis
e69f40553a Version 5.0
- updated changelogs
2020-03-17 15:32:08 +01:00
jannis
0d84407165 Merge updated Hungarian translation 2020-03-17 15:20:01 +01:00
jannis
72650fefb4 Fix workout recording crash 2020-03-17 14:21:59 +01:00
Balázs Úr
815cd764c1 Update Hungarian translation 2020-03-12 10:41:43 +01:00
jannis
e0c3a860b2 German translation 2020-03-11 17:08:04 +01:00
jannis
32724a58f2 Dark mode fixes: make Cycling-brown a bit brighter 2020-03-11 17:00:32 +01:00
jannis
6cf233a5ea Hint when changing themes, dark mode fixes 2020-03-11 16:49:44 +01:00
jannis
5adc2a0536 #48 Dark mode 2020-03-11 16:33:52 +01:00
jannis
4a89e95b41 Correctly display workout comment 2020-03-11 12:24:24 +01:00
jannis
16c42ec350 Selection of seconds when entering workouts 2020-03-11 12:21:51 +01:00
jannis
3f7d590ded Ensure backup compatibility with older versions 2020-03-11 12:14:56 +01:00
jannis
289e25949f Fix bugs in Manual workout adding
- Save end time correctly
- Show hint 'comment' instead of 'custom'
2020-03-11 12:13:39 +01:00
jannis
8665d5ca14 Merge remote-tracking branch 'origin/feature-altitude-correction' 2020-03-11 12:09:42 +01:00
jannis
e6ab3ab871 Merge remote-tracking branch 'origin/4.2' 2020-03-11 12:09:12 +01:00
jannis
4a27bdba26 #52 OpenStreetMap attribution in the record screen 2020-03-10 21:25:27 +01:00
jannis
62570fed27 Dutch translation updates 2020-03-10 21:16:32 +01:00
jannis
a336015818 Merge branch 'feature-manual-workouts'
# Conflicts:
#	app/src/main/AndroidManifest.xml
2020-03-10 21:15:00 +01:00
jannis
3d930cc49e #53 Manually add workouts 2020-03-10 21:13:15 +01:00
albertheres
fe181b5fe4 Update 'metadata/nl/short_description.txt' 2020-03-10 14:48:09 +01:00
jannis
3a22c97018 Altitude correction of workout samples 2020-03-07 19:48:23 +01:00
albertheres
bf6c5d0923 Update 'app/src/main/res/values-nl/strings.xml' 2020-03-05 14:48:07 +01:00
jannis
a26d6a748b #47 Wrong speed values after GPS-signal was lost for some time 2020-03-03 16:00:54 +01:00
jannis
1ed129778b #45 Show recording on the device lock screen 2020-03-03 15:58:39 +01:00
jannis
e978e8f6c2 #51 Detect bluetooth headsets 2020-03-03 15:51:01 +01:00
jannis
e5f3f9f0f7 Display workouts without GPS data 2020-03-03 15:32:21 +01:00
jannis
91d97ae805 Update gradle settings 2020-03-03 15:31:31 +01:00
jannis
5f84e57bf3 Update gradle version 2020-03-03 14:58:14 +01:00
jannis
71033df2c0 Version Code 4.2 2020-02-24 18:12:34 +01:00
jannis
18d04a53d3 Fix new translations 2020-02-24 17:59:04 +01:00
jannis
fbaab9cb1b Merge branch 'master' into develop 2020-02-24 17:54:09 +01:00
jannis
daf3e3fb31 Hungarian translation by urbalazs 2020-02-24 17:53:44 +01:00
jannis
af05b04acb Dutch translation by albertheres 2020-02-24 17:53:31 +01:00
Balázs Úr
1787e30dfa Add Hungarian translation and description 2020-02-18 23:55:40 +01:00
jannis
cd15b57768 More rounding in the height calculation from pressure sensor 2020-01-29 13:42:46 +01:00
albertheres
ee657aff06 Metadata translated in Dutch 2020-01-28 10:42:38 +01:00
albertheres
d628a43a84 Upload files to 'app/src/main/res/values-nl' 2020-01-27 23:40:58 +01:00
jannis
a110964006 Version Code 4.1 2020-01-22 12:15:32 +01:00
jannis
cde4eb5940 #39 Consider ascent value when calculating calories 2020-01-22 12:12:11 +01:00
jannis
e397bc502b German translation 2020-01-22 12:00:25 +01:00
jannis
c83ffd8b4c Fix spanish translation 2020-01-22 11:58:47 +01:00
jannis
112ea2d9f6 Spanish translation of new functions 2020-01-22 11:57:30 +01:00
jannis
782be62f2b #37 Workout list does not refresh properly 2020-01-22 11:56:05 +01:00
serock
15c16ab09d Translation of new functions, minor corrections, II 2020-01-20 22:24:02 -03:00
jannis
2d776a3864 #42 Imperial units wrong 2020-01-17 21:42:28 +01:00
jannis
d75e84d8d9 #36 Pace with the format MM’SS” 2020-01-17 21:39:26 +01:00
jannis
de95affa50 Merge remote-tracking branch 'origin/develop' into develop 2020-01-17 20:58:18 +01:00
jannis
f71c75f180 #29 Option to save backup to internal storage 2020-01-09 16:25:46 +01:00
jannis
a36fc3ae71 Do not backup/restore app settings 2020-01-08 21:54:40 +01:00
jannis
1bd4be8356 update changelog 2020-01-08 17:09:36 +01:00
jannis
6f803f62e4 Fix crash 2020-01-08 17:08:06 +01:00
jannis
20d0b859a2 Fix crash 2020-01-08 17:04:44 +01:00
jannis
fb2f78b9ee fix build issue 2020-01-08 16:25:34 +01:00
jannis
f1a8470a1d Version 4.0
- changelog
2020-01-08 16:23:30 +01:00
jannis
3123e3badb Adjusted Spanish translation 2020-01-08 16:16:41 +01:00
jannis
d6dc49ae84 Merge branch 'master' into develop 2020-01-08 16:14:06 +01:00
jannis
4efa078169 Translation to Spanish 2020-01-08 16:13:37 +01:00
jannis
baf529f9ca New Setting to specify if announcements should be played always or only over headphones 2020-01-08 13:36:03 +01:00
jannis
9089689ce3 Request audio focus for announcements to duck other media 2020-01-08 13:19:20 +01:00
jannis
832f6441cb Clear access token of not valid anymore 2020-01-08 12:37:45 +01:00
jannis
59c1781dd4 #34 [Usability] Make it clear that workout comments can be edited 2020-01-07 16:52:36 +01:00
jannis
3455702375 Extended voice announcements (spoken updates)
- better scalability
- setting for contents
- inform about duration
- inform about GPS status
2020-01-07 16:41:02 +01:00
jannis
f0684647c3 Disabled auto-pause when gps signal is lost 2020-01-07 16:35:40 +01:00
jannis
dfdb2dbac3 Use ThunderForest tiles only to level 19 because of licence to reduce tile requests 2020-01-05 17:01:49 +01:00
jannis
5e5203848d Shut down TTS when recording is stopped 2020-01-05 16:56:31 +01:00
jannis
ce06613b28 Merge branch 'develop' of russok/FitoTrack into develop
Spoken Updates
2020-01-05 16:14:16 +01:00
Ruslan Sokolovski
31528743d3 Cubic bezier produces stupid loops in graphs 2020-01-04 22:50:51 -08:00
Ruslan Sokolovski
a3a4a5c05c reconciliation with the last merge 2020-01-04 19:55:35 -08:00
Ruslan Sokolovski
5bfad8242e be consistent with new merges 2020-01-04 19:40:43 -08:00
Ruslan Sokolovski
acbe5abb05 back to the original pause time - the feature I do not like 2020-01-04 19:23:49 -08:00
Ruslan Sokolovski
4e354a8078 Merge remote-tracking branch 'mylocal/develop' into develop 2020-01-04 19:20:01 -08:00
Ruslan Sokolovski
819eb937db is available at F-Droid 2020-01-04 18:56:14 -08:00
Ruslan Sokolovski
6b224bf264 apparently GPL requires copyright 2020-01-04 18:53:42 -08:00
Ruslan Sokolovski
2b1a0057aa higher zoom is achievable 2020-01-04 18:48:07 -08:00
Ruslan Sokolovski
a9ce6b2fdb false center slows down the map download 2020-01-04 18:47:47 -08:00
jannis
2df035ef78 Code cleanup 2020-01-04 13:57:17 +01:00
Ruslan Sokolovski
7da68b6fc7 create the engine only once 2020-01-03 23:17:40 -08:00
Ruslan Sokolovski
ac3dfe4a7f allow to set the distance for speech updates 2020-01-03 22:29:00 -08:00
jannis
3a0c38fdb4 Bugfix: handle if OSM-OAuth key isn't valid andmore 2020-01-03 15:06:50 +01:00
jannis
f5d2093402 updated Readme 2020-01-03 15:05:52 +01:00
jannis
f5f2be0461 Merge branch 'master' into develop 2020-01-03 14:28:02 +01:00
jannis
787e4333f0 Added a screenshot for documentation 2020-01-03 14:27:58 +01:00
jannis
83c0168108 Migrate the workout saving process to the extra class WorkoutSaver 2020-01-03 14:23:08 +01:00
jannis
7617809897 Update gradle version 2020-01-02 19:30:09 +01:00
Ruslan Sokolovski
b9e16bda25 allow to select the frequency of spoken updates 2020-01-02 01:01:50 -08:00
Ruslan Sokolovski
a0a1f44ef9 give vocal updates 2020-01-01 23:45:43 -08:00
jannis
7b15571e1b Refactor Backup/Restore Process 2019-12-26 00:23:15 +01:00
jannis
db0690a351 Merge remote-tracking branch 'origin/3.0' into develop 2019-12-25 23:56:43 +01:00
serock
96be601641 Translation to Spanish 2019-12-14 17:31:16 -03:00
jannis
5eac86c647 Merge branch '3.0' 2019-12-11 17:44:42 +01:00
jannis
a06a9ec30a Fix Build Issues 2019-12-11 17:38:53 +01:00
jannis
285b76c5d8 Fix Build Issues 2019-12-11 17:24:48 +01:00
jannis
f6ad799212 update Readme
F-Droid link now available
2019-12-11 17:09:00 +01:00
jannis
07b362ddcc Merge branch 'master' into develop 2019-12-08 13:07:32 +01:00
jannis
5fda41149f Version code 3.0 2019-12-08 13:05:50 +01:00
jannis
b7c535c03a Localisation 2019-12-08 13:05:27 +01:00
jannis
b494868f83 Update copyright 2019-12-08 13:05:01 +01:00
jannis
dac2853433 update Readme 2019-12-08 12:49:44 +01:00
jannis
f385fae7cb update Changelog 2019-12-08 12:48:39 +01:00
jannis
05d43de1a7 Merge branch 'feature-upload-osm' into develop 2019-12-08 12:44:02 +01:00
jannis
a186308ba8 Update copyright notices 2019-12-08 12:43:46 +01:00
jannis
4d8f5e53c8 Update Gradle version 2019-12-08 12:01:05 +01:00
jannis
d35c8a7428 #24 Upload workouts to OpenStreetMap as GPS-Trace 2019-12-08 12:00:38 +01:00
jannis
7a61b94696 #23 Automatic Timeout Stop 2019-12-04 22:05:48 +01:00
jannis
a2354e5725 #26 Imperial weight isn't calculated correctly 2019-12-04 21:58:36 +01:00
jannis
b4afba8a9c Better calorie calculation 2019-12-04 21:47:15 +01:00
jannis
a042913381 Delete local files 2019-11-03 18:13:57 +01:00
jannis
02a5cf8bb8 Height/Speed analyse screen: go with the position if not on map 2019-11-03 18:12:29 +01:00
jannis
c4b3c3d2e2 Updated Gradle version 2019-11-03 17:40:11 +01:00
jannis
f664372621 Merge branch '2.0' 2019-11-03 17:38:58 +01:00
jannis
2a5f0f3164 Version 2.1 2019-11-03 17:38:36 +01:00
jannis
7fec3ef250 Fix #25 also for deleting workouts 2019-11-03 17:34:38 +01:00
jannis
4f22a6f500 Fix #25 Workouts not visible after recording has stopped 2019-10-15 12:24:22 +02:00
jannis
b8c4df8551 updated Readme 2019-10-04 16:10:10 +02:00
jannis
7d9d97bab7 updated Readme 2019-10-04 16:09:17 +02:00
158 changed files with 70820 additions and 1389 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# testing
/coverage
# production
/build
/app/build
/app/release
# misc
.*
!.gitignore
# idea
*.iml
# gradle
local.properties

View File

@ -1,9 +1 @@
## 1.0
* Record workouts
* List workouts
* Show workout data (map, speed diagram, energy) + Delete workouts
* Import/Export data
* Export Workouts as GPX
* Change units
* Change weight
*please see [Releases](https://codeberg.org/jannis/FitoTrack/releases).*

View File

@ -24,6 +24,7 @@
Current owners and lead developers are: Ludwig Brinckmann, devemux86
<https://github.com/PhilJay/MPAndroidChart>
Copyright 2019 Philipp Jahoda
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
@ -33,6 +34,7 @@
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.
<https://github.com/Clans/FloatingActionButton>
Copyright 2015 Dmytro Tarianyk
Licensed under the Apache License, Version 2.0 (the "License");
@ -47,6 +49,48 @@
See the License for the specific language governing permissions and
limitations under the License.
<https://github.com/vectorstofinal/geoid_heights>
The MIT License (MIT)
Copyright (c) 2015 vectorstofinal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<https://github.com/westnordost/osmapi>
© 2016-2019 Tobias Zwick. This library is released under the terms of the GNU Lesser General Public License (LGPL).
<https://github.com/mttkay/signpost>
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.
<https://github.com/FasterXML/jackson-core>
This copy of Jackson JSON processor streaming parser/generator is licensed under the

View File

@ -2,7 +2,10 @@
FitoTrack is a mobile app for logging and viewing your workouts. Whether you're running, cycling or hiking, FitoTrack will show you the most important information, with detailed charts and statistics. It is open-source and completely ad-free.
(download links)
<p align="center">
<a href="https://play.google.com/store/apps/details?id=de.tadris.fitness"><img alt="Get it on Google Play" src="https://codeberg.org/jannis/FitoTrack/raw/branch/master/doc/badge-google-play.png" height="75"/></a>
<a href="https://f-droid.org/packages/de.tadris.fitness"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="75"></a>
</p>
## Screenshots
@ -17,19 +20,14 @@ FitoTrack is a mobile app for logging and viewing your workouts. Whether you're
- **View your workouts.** View general information such as date, time, duration, distance, speed and pace. See your route on a map. Work out your level of performance from the speed diagram.
- **Open-Source.** There is neither advertivesment nor tracking, and the source code is open and licensed under the GPLv3.
see the [Feature-List](https://codeberg.org/jannis/FitoTrack/wiki/Features) for a detailed list of features and the [Roadmap](https://codeberg.org/jannis/FitoTrack/wiki/Roadmap) for planned ones.
## Installing
...
Please see the [Userguide](https://codeberg.org/jannis/FitoTrack/wiki/How-to-use) if you have any problems.
## Contributing
You find the app useful and want to contribute to make it even better? Here are some ways you can help:
* **Report issues.** A very simple way of contributing to the project is to report crashes and bugs, as well as suggest possible new features. You can join the [Telegram Group](https://t.me/fitotrack) and share your ideas regarding the app.
* **Share the app.** Tell your friends, family, colleagues about the app, in real life and online, about the app. You could, for example, write a post about FitoTrack on your favorite social media networks.
* **Share the app.** Tell your friends, family, colleagues about the app, in real life and online. You could, for example, write a post about FitoTrack on your favorite social media networks.
* **Write code.** If you're able to write code for Android or just want to help with translations, please refer to the [Contribiution](https://codeberg.org/jannis/FitoTrack/wiki/Contributing) site in the project wiki.
### Donate

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -35,8 +35,8 @@ android {
applicationId "de.tadris.fitness"
minSdkVersion 21
targetSdkVersion 28
versionCode 200
versionName "2.0"
versionCode 500
versionName "5.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@ -49,6 +49,12 @@ android {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
// abortOnError false
}
}
dependencies {
@ -67,15 +73,26 @@ dependencies {
implementation 'org.mapsforge:mapsforge-map-android:0.11.0'
implementation 'com.caverock:androidsvg:1.3'
// UI
implementation 'net.sf.kxml:kxml2:2.3.0'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation 'com.github.clans:fab:1.6.4'
// XML
implementation 'stax:stax-api:1.0.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
// Android Room
def room_version = "2.2.0-beta01"
// File Utils
implementation 'commons-io:commons-io:2.6'
// Upload to OSM
implementation('de.westnordost:osmapi-traces:1.0')
configurations {
compile.exclude group: 'net.sf.kxml', module: 'kxml2' // already included in Android
}
// Android Room Database
def room_version = "2.2.0"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-runtime:$room_version"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -23,32 +23,38 @@
package="de.tadris.fitness">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:appCategory="productivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:appCategory="productivity"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".activity.ShowWorkoutMapActivity"
android:screenOrientation="portrait"></activity>
<activity android:name=".activity.ShowWorkoutMapDiagramActivity"
android:screenOrientation="portrait"></activity>
<activity android:name=".activity.VoiceAnnouncementsSettingsActivity" />
<activity android:name=".activity.SettingsActivity" />
<activity android:name=".activity.EnterWorkoutActivity"/>
<activity
android:name=".activity.ShowWorkoutMapActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.ShowWorkoutMapDiagramActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.ShowWorkoutActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.RecordWorkoutActivity"
android:showOnLockScreen="true"
android:screenOrientation="portrait" />
<activity
android:name=".activity.ListWorkoutsActivity"

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -32,11 +32,12 @@ import java.util.List;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.data.UserPreferences;
import de.tadris.fitness.recording.LocationListener;
import de.tadris.fitness.util.FitoTrackThemes;
import de.tadris.fitness.util.unit.UnitUtils;
public class Instance {
public static final String DATABASE_NAME= "fito-track";
private static final String DATABASE_NAME = "fito-track";
private static Instance instance;
@ -47,20 +48,22 @@ public class Instance {
return instance;
}
public AppDatabase db;
public List<LocationListener.LocationChangeListener> locationChangeListeners= new ArrayList<>();
public UserPreferences userPreferences;
public final AppDatabase db;
public final List<LocationListener.LocationChangeListener> locationChangeListeners = new ArrayList<>();
public final UserPreferences userPreferences;
public final FitoTrackThemes themes;
public boolean pressureAvailable= false;
public float lastPressure= 0;
private Instance(Context context) {
userPreferences= new UserPreferences(context);
themes = new FitoTrackThemes(context);
db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
try{
try {
database.beginTransaction();
database.execSQL("ALTER table workout add descent REAL NOT NULL DEFAULT 0;");
@ -85,7 +88,20 @@ public class Instance {
database.execSQL("DROP TABLE workout_sample2");
database.setTransactionSuccessful();
}finally {
} finally {
database.endTransaction();
}
}
}, new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
try {
database.beginTransaction();
database.execSQL("ALTER table workout add COLUMN edited INTEGER not null default 0");
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}

View File

@ -0,0 +1,221 @@
/*
* Copyright (c) 2020 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.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import de.tadris.fitness.R;
import de.tadris.fitness.data.WorkoutBuilder;
import de.tadris.fitness.data.WorkoutType;
import de.tadris.fitness.dialog.DatePickerFragment;
import de.tadris.fitness.dialog.DurationPickerDialogFragment;
import de.tadris.fitness.dialog.SelectWorkoutTypeDialog;
import de.tadris.fitness.dialog.TimePickerFragment;
import de.tadris.fitness.util.unit.UnitUtils;
public class EnterWorkoutActivity extends InformationActivity implements SelectWorkoutTypeDialog.WorkoutTypeSelectListener, DatePickerFragment.DatePickerCallback, TimePickerFragment.TimePickerCallback, DurationPickerDialogFragment.DurationPickListener {
WorkoutBuilder workoutBuilder = new WorkoutBuilder();
TextView typeTextView, dateTextView, timeTextView, durationTextView;
EditText distanceEditText, commentEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_enter_workout);
initRoot();
addTitle(getString(R.string.info));
setupActionBar();
KeyValueLine typeLine = addKeyValueLine(getString(R.string.type));
typeTextView = typeLine.value;
typeLine.lineRoot.setOnClickListener(v -> showTypeSelection());
distanceEditText = new EditText(this);
distanceEditText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
distanceEditText.setSingleLine(true);
distanceEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
distanceEditText.setOnEditorActionListener((v, actionId, event) -> {
// If the User clicks on the finish button on the keyboard, continue by showing the date selection
if (actionId == EditorInfo.IME_ACTION_SEARCH ||
actionId == EditorInfo.IME_ACTION_DONE ||
event != null &&
event.getAction() == KeyEvent.ACTION_DOWN &&
event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
if (event == null || !event.isShiftPressed()) {
showDateSelection();
return true;
}
}
return false;
});
addKeyValueLine(getString(R.string.workoutDistance), distanceEditText, UnitUtils.CHOSEN_SYSTEM.getLongDistanceUnit());
KeyValueLine dateLine = addKeyValueLine(getString(R.string.workoutDate));
dateLine.lineRoot.setOnClickListener(v -> showDateSelection());
dateTextView = dateLine.value;
KeyValueLine timeLine = addKeyValueLine(getString(R.string.workoutStartTime));
timeLine.lineRoot.setOnClickListener(v -> showTimeSelection());
timeTextView = timeLine.value;
KeyValueLine durationLine = addKeyValueLine(getString(R.string.workoutDuration));
durationLine.lineRoot.setOnClickListener(v -> showDurationSelection());
durationTextView = durationLine.value;
addTitle(getString(R.string.comment));
commentEditText = new EditText(this);
commentEditText.setSingleLine(true);
root.addView(commentEditText);
updateTextViews();
}
private void saveWorkout() {
workoutBuilder.setComment(commentEditText.getText().toString());
try {
workoutBuilder.setLength((int) (Double.parseDouble(distanceEditText.getText().toString()) * 1000));
} catch (NumberFormatException ignored) {
distanceEditText.requestFocus();
distanceEditText.setError(getString(R.string.errorEnterValidNumber));
return;
}
if (workoutBuilder.getStart().getTimeInMillis() > System.currentTimeMillis()) {
Toast.makeText(this, R.string.errorWorkoutAddFuture, Toast.LENGTH_LONG).show();
return;
}
if (workoutBuilder.getDuration() < 1000) {
Toast.makeText(this, R.string.errorEnterValidDuration, Toast.LENGTH_LONG).show();
return;
}
ShowWorkoutActivity.selectedWorkout = workoutBuilder.insertWorkout(this);
startActivity(new Intent(this, ShowWorkoutActivity.class));
finish();
}
private void updateTextViews() {
typeTextView.setText(getString(workoutBuilder.getWorkoutType().title));
dateTextView.setText(SimpleDateFormat.getDateInstance().format(workoutBuilder.getStart().getTime()));
timeTextView.setText(SimpleDateFormat.getTimeInstance().format(workoutBuilder.getStart().getTime()));
durationTextView.setText(UnitUtils.getHourMinuteSecondTime(workoutBuilder.getDuration()));
}
private void showTypeSelection() {
new SelectWorkoutTypeDialog(this, this).show();
}
@Override
public void onSelectWorkoutType(WorkoutType workoutType) {
workoutBuilder.setWorkoutType(workoutType);
updateTextViews();
distanceEditText.requestFocus();
}
private void showDateSelection() {
DatePickerFragment fragment = new DatePickerFragment();
fragment.callback = this;
fragment.show(getFragmentManager(), "datePicker");
}
@Override
public void onDatePick(int year, int month, int day) {
workoutBuilder.getStart().set(year, month, day);
updateTextViews();
showTimeSelection();
}
private void showTimeSelection() {
TimePickerFragment fragment = new TimePickerFragment();
fragment.callback = this;
fragment.show(getFragmentManager(), "timePicker");
}
@Override
public void onTimePick(int hour, int minute) {
workoutBuilder.getStart().set(Calendar.HOUR_OF_DAY, hour);
workoutBuilder.getStart().set(Calendar.MINUTE, minute);
workoutBuilder.getStart().set(Calendar.SECOND, 0);
updateTextViews();
showDurationSelection();
}
private void showDurationSelection() {
DurationPickerDialogFragment fragment = new DurationPickerDialogFragment(this, this, workoutBuilder.getDuration());
fragment.listener = this;
fragment.initialDuration = workoutBuilder.getDuration();
fragment.show();
}
@Override
public void onDurationPick(long duration) {
workoutBuilder.setDuration(duration);
updateTextViews();
commentEditText.requestFocus();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.enter_workout_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.actionEnterWorkoutAdd:
saveWorkout();
return true;
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void setupActionBar() {
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
}
@Override
void initRoot() {
root = findViewById(R.id.enterWorkoutRoot);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,48 +19,35 @@
package de.tadris.fitness.activity;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.ActivityCompat;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
abstract public class FitoTrackActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(Instance.getInstance(this).themes.getDefaultTheme());
}
protected int getThemePrimaryColor() {
int getThemePrimaryColor() {
final TypedValue value = new TypedValue ();
getTheme().resolveAttribute (android.R.attr.colorPrimary, value, true);
return value.data;
}
protected void shareFile(Uri uri){
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setDataAndType(uri, getContentResolver().getType(uri));
intentShareFile.putExtra(Intent.EXTRA_STREAM, uri);
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intentShareFile, getString(R.string.shareFile)));
Log.d("Export", uri.toString());
Log.d("Export", getContentResolver().getType(uri));
try {
Log.d("Export", new BufferedInputStream(getContentResolver().openInputStream(uri)).toString());
} catch (FileNotFoundException e) {
}
}
protected void showErrorDialog(Exception e, @StringRes int title, @StringRes int message){
void showErrorDialog(Exception e, @StringRes int title, @StringRes int message) {
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(getString(message) + "\n\n" + e.getMessage())
@ -68,5 +55,16 @@ abstract public class FitoTrackActivity extends Activity {
.create().show();
}
protected void requestStoragePermissions() {
if (!hasStoragePermission()) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 10);
}
}
protected boolean hasStoragePermission() {
return ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright (c) 2020 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.AlertDialog;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.RingtonePreference;
import android.text.TextUtils;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.util.unit.UnitUtils;
public abstract class FitoTrackSettingsActivity extends PreferenceActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(Instance.getInstance(this).themes.getDefaultTheme());
super.onCreate(savedInstanceState);
}
protected void showErrorDialog(Exception e, @StringRes int title, @StringRes int message) {
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(getString(message) + "\n\n" + e.getMessage())
.setPositiveButton(R.string.okay, null)
.create().show();
}
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
protected static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display value
// using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone display
// name.
String name = ringtone.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
};
protected static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
@Override
protected void onDestroy() {
super.onDestroy();
UnitUtils.setUnit(this);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onMenuItemSelected(featureId, item);
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) 2020 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.graphics.Typeface;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import de.tadris.fitness.R;
public abstract class InformationActivity extends FitoTrackActivity {
ViewGroup root;
protected void addTitle(String title) {
TextView textView = new TextView(this);
textView.setText(title);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
textView.setTextColor(getThemePrimaryColor());
textView.setTypeface(Typeface.DEFAULT_BOLD);
textView.setAllCaps(true);
textView.setPadding(0, 20, 0, 0);
root.addView(textView);
}
protected TextView addText(String text, boolean themeColor) {
TextView textView = createTextView(text, themeColor);
root.addView(textView);
return textView;
}
protected TextView createTextView(String text) {
return createTextView(text, false);
}
protected TextView createTextView(String text, boolean themeColor) {
TextView textView = new TextView(this);
textView.setText(text);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
if (themeColor) {
textView.setTextColor(getThemePrimaryColor());
} else {
textView.setTextColor(getResources().getColor(R.color.textLighterBlack));
}
textView.setPadding(0, 20, 0, 0);
return textView;
}
protected void addKeyValue(String key1, String value1) {
addKeyValue(key1, value1, "", "");
}
protected void addKeyValue(String key1, String value1, String key2, String value2) {
View v = getLayoutInflater().inflate(R.layout.show_entry, root, false);
TextView title1 = v.findViewById(R.id.v1title);
TextView title2 = v.findViewById(R.id.v2title);
TextView v1 = v.findViewById(R.id.v1value);
TextView v2 = v.findViewById(R.id.v2value);
title1.setText(key1);
title2.setText(key2);
v1.setText(value1);
v2.setText(value2);
root.addView(v);
}
protected KeyValueLine addKeyValueLine(String key) {
return addKeyValueLine(key, "");
}
protected KeyValueLine addKeyValueLine(String key, String value) {
return addKeyValueLine(key, null, value);
}
protected KeyValueLine addKeyValueLine(String key, @Nullable View view) {
return addKeyValueLine(key, view, "");
}
protected KeyValueLine addKeyValueLine(String key, @Nullable View view, String value) {
View v = getLayoutInflater().inflate(R.layout.enter_workout_line, root, false);
TextView keyView = v.findViewById(R.id.lineKey);
TextView valueView = v.findViewById(R.id.lineValue);
LinearLayout customViewRoot = v.findViewById(R.id.lineViewRoot);
keyView.setText(key);
valueView.setText(value);
if (view != null) {
customViewRoot.addView(view);
}
root.addView(v);
return new KeyValueLine(v, keyView, valueView, view);
}
public static class KeyValueLine {
public View lineRoot;
public TextView key;
public TextView value;
public View customView;
public KeyValueLine(View lineRoot, TextView key, TextView value, View customView) {
this.lineRoot = lineRoot;
this.key = key;
this.value = value;
this.customView = customView;
}
}
abstract void initRoot();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -42,12 +42,12 @@ public class LauncherActivity extends Activity {
new Handler().postDelayed(this::init, 100);
}
void init(){
private void init() {
Instance.getInstance(this);
start();
}
void start(){
private void start() {
startActivity(new Intent(this, ListWorkoutsActivity.class));
finish();
overridePendingTransition(R.anim.fade_in, R.anim.stay);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,7 +19,6 @@
package de.tadris.fitness.activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
@ -37,16 +36,17 @@ import com.github.clans.fab.FloatingActionMenu;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutType;
import de.tadris.fitness.util.DialogUtils;
import de.tadris.fitness.view.WorkoutAdapter;
public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.WorkoutAdapterListener {
public class ListWorkoutsActivity extends FitoTrackActivity implements WorkoutAdapter.WorkoutAdapterListener {
private RecyclerView listView;
private RecyclerView.Adapter adapter;
private RecyclerView.LayoutManager layoutManager;
private FloatingActionMenu menu;
Workout[] workouts;
private Workout[] workouts;
@Override
@ -63,24 +63,21 @@ public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.Wor
menu= findViewById(R.id.workoutListMenu);
menu.setOnMenuButtonLongClickListener(v -> {
if(workouts.length > 0){
startRecording(workouts[0].workoutType);
startRecording(workouts[0].getWorkoutType());
return true;
}else{
return false;
}
});
findViewById(R.id.workoutListRecordRunning).setOnClickListener(v -> startRecording(Workout.WORKOUT_TYPE_RUNNING));
findViewById(R.id.workoutListRecordHiking) .setOnClickListener(v -> startRecording(Workout.WORKOUT_TYPE_HIKING));
findViewById(R.id.workoutListRecordCycling).setOnClickListener(v -> startRecording(Workout.WORKOUT_TYPE_CYCLING));
loadData();
findViewById(R.id.workoutListRecordRunning).setOnClickListener(v -> startRecording(WorkoutType.RUNNING));
findViewById(R.id.workoutListRecordHiking).setOnClickListener(v -> startRecording(WorkoutType.HIKING));
findViewById(R.id.workoutListRecordCycling).setOnClickListener(v -> startRecording(WorkoutType.CYCLING));
findViewById(R.id.workoutListEnter).setOnClickListener(v -> startEnterWorkoutActivity());
checkFirstStart();
adapter= new WorkoutAdapter(workouts, this);
listView.setAdapter(adapter);
refresh();
}
private void checkFirstStart(){
@ -96,7 +93,13 @@ public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.Wor
}
}
public void startRecording(String activity){
private void startEnterWorkoutActivity() {
menu.close(true);
final Intent intent = new Intent(this, EnterWorkoutActivity.class);
new Handler().postDelayed(() -> startActivity(intent), 300);
}
private void startRecording(WorkoutType activity) {
menu.close(true);
RecordWorkoutActivity.ACTIVITY= activity;
final Intent intent= new Intent(this, RecordWorkoutActivity.class);
@ -107,12 +110,11 @@ public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.Wor
public void onResume() {
super.onResume();
loadData();
adapter.notifyDataSetChanged();
refresh();
}
@Override
public void onItemClick(Workout workout) {
public void onItemClick(int pos, Workout workout) {
ShowWorkoutActivity.selectedWorkout= workout;
startActivity(new Intent(this, ShowWorkoutActivity.class));
}
@ -121,15 +123,24 @@ public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.Wor
public void onItemLongClick(int pos, Workout workout) {
DialogUtils.showDeleteWorkoutDialog(this, () -> {
Instance.getInstance(ListWorkoutsActivity.this).db.workoutDao().deleteWorkout(workout);
loadData();
adapter.notifyItemRemoved(pos);
refresh();
});
}
private void refresh() {
loadData();
refreshAdapter();
}
private void loadData(){
workouts= Instance.getInstance(this).db.workoutDao().getWorkouts();
}
private void refreshAdapter(){
adapter= new WorkoutAdapter(workouts, this);
listView.setAdapter(adapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
@ -141,10 +152,9 @@ public class ListWorkoutsActivity extends Activity implements WorkoutAdapter.Wor
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id){
case R.id.actionOpenSettings:
startActivity(new Intent(this, SettingsActivity.class));
return true;
if (id == R.id.actionOpenSettings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -35,6 +35,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
@ -53,54 +54,62 @@ 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.WorkoutType;
import de.tadris.fitness.map.MapManager;
import de.tadris.fitness.map.tilesource.TileSources;
import de.tadris.fitness.recording.LocationListener;
import de.tadris.fitness.recording.PressureService;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.ThemeManager;
import de.tadris.fitness.recording.announcement.AnnouncementGPSStatus;
import de.tadris.fitness.recording.announcement.VoiceAnnouncements;
import de.tadris.fitness.util.unit.UnitUtils;
public class RecordWorkoutActivity extends FitoTrackActivity implements LocationListener.LocationChangeListener, WorkoutRecorder.GpsStateChangedListener {
public class RecordWorkoutActivity extends FitoTrackActivity implements LocationListener.LocationChangeListener, WorkoutRecorder.WorkoutRecorderListener, VoiceAnnouncements.VoiceAnnouncementCallback {
public static String ACTIVITY= Workout.WORKOUT_TYPE_RUNNING;
public static WorkoutType ACTIVITY = WorkoutType.OTHER;
MapView mapView;
TileDownloadLayer downloadLayer;
WorkoutRecorder recorder;
Polyline polyline;
List<LatLong> latLongList= new ArrayList<>();
InfoViewHolder[] infoViews= new InfoViewHolder[4];
TextView timeView, gpsStatusView;
View waitingForGPSOverlay;
boolean gpsFound= false;
boolean isResumed= false;
private Handler mHandler= new Handler();
PowerManager.WakeLock wakeLock;
Intent locationListener;
Intent pressureService;
private MapView mapView;
private TileDownloadLayer downloadLayer;
private WorkoutRecorder recorder;
private Polyline polyline;
private final List<LatLong> latLongList = new ArrayList<>();
private final InfoViewHolder[] infoViews = new InfoViewHolder[4];
private TextView timeView;
private TextView gpsStatusView;
private TextView attribution;
private View waitingForGPSOverlay;
private boolean gpsFound = false;
private boolean isResumed = false;
private final Handler mHandler = new Handler();
private PowerManager.WakeLock wakeLock;
private Intent locationListener;
private Intent pressureService;
private boolean saved= false;
private VoiceAnnouncements voiceAnnouncements;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(ThemeManager.getThemeByWorkoutType(ACTIVITY));
setTheme(Instance.getInstance(this).themes.getWorkoutTypeTheme(ACTIVITY));
setContentView(R.layout.activity_record_workout);
setTitle(R.string.recordWorkout);
setupMap();
((ViewGroup)findViewById(R.id.recordMapViewrRoot)).addView(mapView);
((ViewGroup) findViewById(R.id.recordMapViewerRoot)).addView(mapView);
waitingForGPSOverlay= findViewById(R.id.recorderWaitingOverlay);
waitingForGPSOverlay.setVisibility(View.VISIBLE);
attribution = findViewById(R.id.recordMapAttribution);
checkPermissions();
recorder= new WorkoutRecorder(this, ACTIVITY, this);
recorder.start();
voiceAnnouncements = new VoiceAnnouncements(this, this);
infoViews[0]= new InfoViewHolder(findViewById(R.id.recordInfo1Title), findViewById(R.id.recordInfo1Value));
infoViews[1]= new InfoViewHolder(findViewById(R.id.recordInfo2Title), findViewById(R.id.recordInfo2Value));
infoViews[2]= new InfoViewHolder(findViewById(R.id.recordInfo3Title), findViewById(R.id.recordInfo3Value));
@ -129,7 +138,9 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
waitingForGPSOverlay.clearAnimation();
waitingForGPSOverlay.animate().alpha(0f).setDuration(1000).setListener(new Animator.AnimatorListener() {
@Override public void onAnimationStart(Animator animator) { }
@Override public void onAnimationCancel(Animator animator) { }
@Override public void onAnimationRepeat(Animator animator) { }
@Override
@ -137,17 +148,16 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
waitingForGPSOverlay.setVisibility(View.GONE);
}
}).start();
hideOSMAttribution();
}
private void hideOSMAttribution() {
attribution.animate().alpha(0f).setDuration(1000).setStartDelay(5000).start();
}
private void setupMap(){
this.mapView= new MapView(this);
TileSources.Purpose purpose;
if(ACTIVITY.equals(Workout.WORKOUT_TYPE_CYCLING)){
purpose= TileSources.Purpose.CYCLING;
}else{
purpose= TileSources.Purpose.OUTDOOR;
}
downloadLayer= MapManager.setupMap(mapView, purpose);
downloadLayer = MapManager.setupMap(mapView);
}
private void updateLine(){
@ -168,9 +178,7 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
try{
while (recorder.isActive()){
Thread.sleep(1000);
if(isResumed){
mHandler.post(this::updateDescription);
}
mHandler.post(this::updateDescription);
}
}catch (InterruptedException e){
e.printStackTrace();
@ -178,15 +186,23 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
}).start();
}
int i= 0;
private void updateDescription(){
i++;
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(Math.min(100d, recorder.getAvgSpeed())));
infoViews[3].setText(getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(recorder.getPauseDuration()));
private void updateDescription() {
long duration = recorder.getDuration();
int distanceInMeters = recorder.getDistanceInMeters();
final String distanceCaption = getString(R.string.workoutDistance);
final String distance = UnitUtils.getDistance(distanceInMeters);
final String avgSpeed = UnitUtils.getSpeed(Math.min(100d, recorder.getAvgSpeed()));
if (isResumed) {
timeView.setText(UnitUtils.getHourMinuteSecondTime(duration));
infoViews[0].setText(distanceCaption, distance);
infoViews[1].setText(getString(R.string.workoutBurnedEnergy), recorder.getCalories() + " kcal");
infoViews[2].setText(getString(R.string.workoutAvgSpeedShort), avgSpeed);
infoViews[3].setText(getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(recorder.getPauseDuration()));
}
voiceAnnouncements.check(recorder);
}
private void stop(){
@ -235,14 +251,14 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
.create().show();
}
void checkPermissions(){
private void checkPermissions() {
if (!hasPermission()) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 10);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 10);
}
}
public boolean hasPermission(){
private boolean hasPermission() {
return ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
@ -253,12 +269,12 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
}
}
public void stopListener(){
private void stopListener() {
stopService(locationListener);
stopService(pressureService);
}
public void startListener(){
private void startListener() {
if(locationListener == null){
locationListener= new Intent(this, LocationListener.class);
pressureService= new Intent(this, PressureService.class);
@ -304,9 +320,15 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
@Override
protected void onDestroy() {
recorder.stop();
saveIfNotSaved(); // Important
saveIfNotSaved(); // Important to save
// Clear map
mapView.destroyAll();
AndroidGraphicFactory.clearResourceMemoryCache();
// Shutdown TTS
voiceAnnouncements.destroy();
super.onDestroy();
if(wakeLock.isHeld()){
wakeLock.release();
@ -324,10 +346,18 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
public void onResume(){
super.onResume();
enableLockScreenVisibility();
downloadLayer.onResume();
isResumed= true;
}
private void enableLockScreenVisibility() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
@ -362,13 +392,27 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
gpsFound= true;
hideWaitOverlay();
}
AnnouncementGPSStatus announcement = new AnnouncementGPSStatus(RecordWorkoutActivity.this);
if (announcement.isEnabled()) {
if (oldState == WorkoutRecorder.GpsState.SIGNAL_LOST) { // GPS Signal found
voiceAnnouncements.speak(announcement.getSpokenGPSFound());
} else if (state == WorkoutRecorder.GpsState.SIGNAL_LOST) {
voiceAnnouncements.speak(announcement.getSpokenGPSLost());
}
}
});
}
public static class InfoViewHolder{
TextView titleView, valueView;
@Override
public void onVoiceAnnouncementIsReady(boolean available) {
}
public InfoViewHolder(TextView titleView, TextView valueView) {
static class InfoViewHolder {
final TextView titleView;
final TextView valueView;
InfoViewHolder(TextView titleView, TextView valueView) {
this.titleView = titleView;
this.valueView = valueView;
}
@ -379,5 +423,8 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
}
}
@Override
public void onAutoStop() {
finish();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -25,127 +25,31 @@ import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.RingtonePreference;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.NumberPicker;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import de.tadris.fitness.R;
import de.tadris.fitness.util.export.Exporter;
import de.tadris.fitness.export.BackupController;
import de.tadris.fitness.export.RestoreController;
import de.tadris.fitness.recording.announcement.VoiceAnnouncements;
import de.tadris.fitness.util.FileUtils;
import de.tadris.fitness.util.unit.UnitUtils;
import de.tadris.fitness.view.ProgressDialogController;
public class SettingsActivity extends PreferenceActivity {
public class SettingsActivity extends FitoTrackSettingsActivity {
protected void shareFile(Uri uri){
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setDataAndType(uri, getContentResolver().getType(uri));
intentShareFile.putExtra(Intent.EXTRA_STREAM, uri);
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intentShareFile, getString(R.string.shareFile)));
Log.d("Export", uri.toString());
Log.d("Export", getContentResolver().getType(uri));
try {
Log.d("Export", new BufferedInputStream(getContentResolver().openInputStream(uri)).toString());
} catch (FileNotFoundException e) {
}
}
protected void showErrorDialog(Exception e, @StringRes int title, @StringRes int message){
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(getString(message) + "\n\n" + e.getMessage())
.setPositiveButton(R.string.okay, null)
.create().show();
}
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display value
// using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone display
// name.
String name = ringtone.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
};
private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
private Handler mHandler= new Handler();
private final Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -158,22 +62,62 @@ public class SettingsActivity extends PreferenceActivity {
bindPreferenceSummaryToValue(findPreference("unitSystem"));
bindPreferenceSummaryToValue(findPreference("mapStyle"));
bindPreferenceSummaryToValue(findPreference("themeSetting"));
findPreference("themeSetting").setOnPreferenceChangeListener((preference, newValue) -> {
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, newValue);
Toast.makeText(SettingsActivity.this, R.string.hintRestart, Toast.LENGTH_LONG).show();
return true;
});
findPreference("weight").setOnPreferenceClickListener(preference -> showWeightPicker());
findPreference("import").setOnPreferenceClickListener(preference -> showImportDialog());
findPreference("export").setOnPreferenceClickListener(preference -> showExportDialog());
findPreference("weight").setOnPreferenceClickListener(preference -> {
showWeightPicker();
return true;
});
findPreference("speech").setOnPreferenceClickListener(preference -> {
checkTTSandShowConfig();
return true;
});
findPreference("import").setOnPreferenceClickListener(preference -> {
showImportDialog();
return true;
});
findPreference("export").setOnPreferenceClickListener(preference -> {
showExportDialog();
return true;
});
}
private boolean showExportDialog(){
private VoiceAnnouncements voiceAnnouncements;
private void checkTTSandShowConfig() {
voiceAnnouncements = new VoiceAnnouncements(this, available -> {
if (available) {
showSpeechConfig();
} else {
// TextToSpeech is not available
Toast.makeText(SettingsActivity.this, R.string.ttsNotAvailable, Toast.LENGTH_LONG).show();
}
if (voiceAnnouncements != null) {
voiceAnnouncements.destroy();
}
});
}
private void showSpeechConfig() {
startActivity(new Intent(this, VoiceAnnouncementsSettingsActivity.class));
}
private void showExportDialog() {
if (!hasPermission()) {
requestPermissions();
return;
}
new AlertDialog.Builder(this)
.setTitle(R.string.exportData)
.setMessage(R.string.exportDataSummary)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.backup, (dialog, which) -> {
exportBackup();
}).create().show();
return true;
.setPositiveButton(R.string.backup, (dialog, which) -> exportBackup()).create().show();
}
private void exportBackup(){
@ -182,15 +126,18 @@ public class SettingsActivity extends PreferenceActivity {
new Thread(() -> {
try{
String file= getFilesDir().getAbsolutePath() + "/shared/backup.ftb";
new File(file).getParentFile().mkdirs();
File parent = new File(file).getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Cannot write");
}
Uri uri= FileProvider.getUriForFile(getBaseContext(), "de.tadris.fitness.fileprovider", new File(file));
Exporter.exportData(getBaseContext(), new File(file),
(progress, action) -> mHandler.post(() -> dialogController.setProgress(progress, action)));
BackupController backupController= new BackupController(getBaseContext(), new File(file), (progress, action) -> mHandler.post(() -> dialogController.setProgress(progress, action)));
backupController.exportData();
mHandler.post(() -> {
dialogController.cancel();
shareFile(uri);
FileUtils.saveOrShareFile(this, uri, "ftb");
});
}catch (Exception e){
e.printStackTrace();
@ -202,28 +149,25 @@ public class SettingsActivity extends PreferenceActivity {
}).start();
}
private boolean showImportDialog(){
private void showImportDialog() {
if(!hasPermission()){
requestPermissions();
return true;
return;
}
new AlertDialog.Builder(this)
.setTitle(R.string.importBackup)
.setMessage(R.string.importBackupMessage)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.restore, (dialog, which) -> {
importBackup();
}).create().show();
return true;
.setPositiveButton(R.string.restore, (dialog, which) -> importBackup()).create().show();
}
void requestPermissions(){
private void requestPermissions() {
if (!hasPermission()) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 10);
}
}
public boolean hasPermission(){
private boolean hasPermission() {
return ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
@ -239,12 +183,10 @@ public class SettingsActivity extends PreferenceActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case FILE_SELECT_CODE:
if (resultCode == RESULT_OK){
importBackup(data.getData());
}
break;
if (requestCode == FILE_SELECT_CODE) {
if (resultCode == RESULT_OK) {
importBackup(data.getData());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@ -254,10 +196,10 @@ public class SettingsActivity extends PreferenceActivity {
dialogController.show();
new Thread(() -> {
try{
Exporter.importData(getBaseContext(), uri,
RestoreController restoreController= new RestoreController(getBaseContext(), uri,
(progress, action) -> mHandler.post(() -> dialogController.setProgress(progress, action)));
restoreController.restoreData();
// DO on backup finished
mHandler.post(dialogController::cancel);
}catch (Exception e){
e.printStackTrace();
@ -269,7 +211,9 @@ public class SettingsActivity extends PreferenceActivity {
}).start();
}
private boolean showWeightPicker() {
private void showWeightPicker() {
UnitUtils.setUnit(this); // Maybe the user changed unit system
final AlertDialog.Builder d = new AlertDialog.Builder(this);
final SharedPreferences preferences= PreferenceManager.getDefaultSharedPreferences(this);
d.setTitle(getString(R.string.pref_weight));
@ -278,7 +222,8 @@ public class SettingsActivity extends PreferenceActivity {
np.setMaxValue((int) UnitUtils.CHOSEN_SYSTEM.getWeightFromKilogram(150));
np.setMinValue((int) UnitUtils.CHOSEN_SYSTEM.getWeightFromKilogram(20));
np.setFormatter(value -> value + " " + UnitUtils.CHOSEN_SYSTEM.getWeightUnit());
np.setValue(preferences.getInt("weight", 80));
final String preferenceVariable = "weight";
np.setValue((int)Math.round(UnitUtils.CHOSEN_SYSTEM.getWeightFromKilogram(preferences.getInt(preferenceVariable, 80))));
np.setWrapSelectorWheel(false);
d.setView(v);
@ -287,12 +232,10 @@ public class SettingsActivity extends PreferenceActivity {
d.setPositiveButton(R.string.okay, (dialog, which) -> {
int unitValue= np.getValue();
int kilograms= (int)Math.round(UnitUtils.CHOSEN_SYSTEM.getKilogramFromUnit(unitValue));
preferences.edit().putInt("weight", kilograms).apply();
preferences.edit().putInt(preferenceVariable, kilograms).apply();
});
d.create().show();
return true;
}
/**
@ -306,24 +249,4 @@ public class SettingsActivity extends PreferenceActivity {
}
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
UnitUtils.setUnit(this);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onMenuItemSelected(featureId, item);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -21,31 +21,40 @@ package de.tadris.fitness.activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.osm.OAuthAuthentication;
import de.tadris.fitness.osm.OsmTraceUploader;
import de.tadris.fitness.util.DialogUtils;
import de.tadris.fitness.util.FileUtils;
import de.tadris.fitness.util.gpx.GpxExporter;
import de.tadris.fitness.util.unit.UnitUtils;
import de.tadris.fitness.view.ProgressDialogController;
import de.westnordost.osmapi.traces.GpsTraceDetails;
import oauth.signpost.OAuthConsumer;
public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.WorkoutDeleter {
TextView commentView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -54,14 +63,13 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
initBeforeContent();
setContentView(R.layout.activity_show_workout);
root= findViewById(R.id.showWorkoutRoot);
initRoot();
initAfterContent();
addText(getString(R.string.comment) + ": " + workout.comment).setOnClickListener(v -> {
TextView textView= (TextView)v;
openEditCommentDialog(textView);
});
commentView = addText("", true);
commentView.setOnClickListener(v -> openEditCommentDialog());
updateCommentText();
addTitle(getString(R.string.workoutTime));
addKeyValue(getString(R.string.workoutDate), getDate());
@ -72,110 +80,91 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
addKeyValue(getString(R.string.workoutDistance), UnitUtils.getDistance(workout.length), getString(R.string.workoutPace), UnitUtils.getPace(workout.avgPace));
addTitle(getString(R.string.workoutRoute));
if (hasSamples()) {
addTitle(getString(R.string.workoutRoute));
addMap();
addMap();
map.setClickable(false);
mapRoot.setOnClickListener(v -> startActivity(new Intent(ShowWorkoutActivity.this, ShowWorkoutMapActivity.class)));
}
map.setClickable(false);
mapRoot.setOnClickListener(v -> startActivity(new Intent(ShowWorkoutActivity.this, ShowWorkoutMapActivity.class)));
addTitle(getString(R.string.workoutSpeed));
addKeyValue(getString(R.string.workoutAvgSpeed), UnitUtils.getSpeed(workout.avgSpeed),
addKeyValue(getString(R.string.workoutAvgSpeedShort), UnitUtils.getSpeed(workout.avgSpeed),
getString(R.string.workoutTopSpeed), UnitUtils.getSpeed(workout.topSpeed));
addSpeedDiagram();
if (hasSamples()) {
speedDiagram.setOnClickListener(v -> startDiagramActivity(ShowWorkoutMapDiagramActivity.DIAGRAM_TYPE_SPEED));
addSpeedDiagram();
speedDiagram.setOnClickListener(v -> startDiagramActivity(ShowWorkoutMapDiagramActivity.DIAGRAM_TYPE_SPEED));
}
addTitle(getString(R.string.workoutBurnedEnergy));
addKeyValue(getString(R.string.workoutTotalEnergy), workout.calorie + " kcal",
getString(R.string.workoutEnergyConsumption), UnitUtils.getRelativeEnergyConsumption((double)workout.calorie / ((double)workout.length / 1000)));
addTitle(getString(R.string.height));
if (hasSamples()) {
addTitle(getString(R.string.height));
addKeyValue(getString(R.string.workoutAscent), UnitUtils.getDistance((int)workout.ascent),
getString(R.string.workoutDescent), UnitUtils.getDistance((int)workout.descent));
addKeyValue(getString(R.string.workoutAscent), UnitUtils.getDistance((int) workout.ascent),
getString(R.string.workoutDescent), UnitUtils.getDistance((int) workout.descent));
addHeightDiagram();
addHeightDiagram();
heightDiagram.setOnClickListener(v -> startDiagramActivity(ShowWorkoutMapDiagramActivity.DIAGRAM_TYPE_HEIGHT));
heightDiagram.setOnClickListener(v -> startDiagramActivity(ShowWorkoutMapDiagramActivity.DIAGRAM_TYPE_HEIGHT));
}
}
void startDiagramActivity(String diagramType){
private void startDiagramActivity(String diagramType) {
ShowWorkoutMapDiagramActivity.DIAGRAM_TYPE= diagramType;
startActivity(new Intent(ShowWorkoutActivity.this, ShowWorkoutMapDiagramActivity.class));
}
void openEditCommentDialog(final TextView change){
private void openEditCommentDialog() {
final EditText editText= new EditText(this);
editText.setText(workout.comment);
editText.setSingleLine(true);
new AlertDialog.Builder(this)
.setTitle(R.string.enterComment)
.setPositiveButton(R.string.okay, (dialog, which) -> changeComment(editText.getText().toString(), change))
.setPositiveButton(R.string.okay, (dialog, which) -> changeComment(editText.getText().toString()))
.setView(editText).create().show();
}
void changeComment(String comment, TextView onChange){
private void changeComment(String comment) {
workout.comment= comment;
Instance.getInstance(this).db.workoutDao().updateWorkout(workout);
onChange.setText(getString(R.string.comment) + ": " + workout.comment);
updateCommentText();
}
String getDate(){
private void updateCommentText() {
String str = "";
if (workout.edited) {
str += getString(R.string.workoutEdited);
}
if (workout.comment != null && workout.comment.length() > 0) {
if (str.length() > 0) {
str += "\n";
}
str += getString(R.string.comment) + ": " + workout.comment;
}
if (str.length() == 0) {
str = getString(R.string.noComment);
}
commentView.setText(str);
}
private String getDate() {
return SimpleDateFormat.getDateInstance().format(new Date(workout.start));
}
void addTitle(String title){
TextView textView= new TextView(this);
textView.setText(title);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
textView.setTextColor(getThemePrimaryColor());
textView.setTypeface(Typeface.DEFAULT_BOLD);
textView.setAllCaps(true);
textView.setPadding(0, 20, 0, 0);
root.addView(textView);
}
TextView addText(String text){
TextView textView= new TextView(this);
textView.setText(text);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
textView.setTextColor(getThemePrimaryColor());
textView.setPadding(0, 20, 0, 0);
root.addView(textView);
return textView;
}
void addKeyValue(String key1, String value1){
addKeyValue(key1, value1, "", "");
}
void addKeyValue(String key1, String value1, String key2, String value2){
View v= getLayoutInflater().inflate(R.layout.show_entry, root, false);
TextView title1= v.findViewById(R.id.v1title);
TextView title2= v.findViewById(R.id.v2title);
TextView v1= v.findViewById(R.id.v1value);
TextView v2= v.findViewById(R.id.v2value);
title1.setText(key1);
title2.setText(key2);
v1.setText(value1);
v2.setText(value2);
root.addView(v);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
@ -193,25 +182,81 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
}
private void exportToGpx(){
if (!hasStoragePermission()) {
requestStoragePermissions();
return;
}
ProgressDialogController dialogController= new ProgressDialogController(this, getString(R.string.exporting));
dialogController.setIndeterminate(true);
dialogController.show();
new Thread(() -> {
try{
String file= getFilesDir().getAbsolutePath() + "/shared/workout.gpx";
new File(file).getParentFile().mkdirs();
File parent = new File(file).getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Cannot write to " + file);
}
Uri uri= FileProvider.getUriForFile(getBaseContext(), "de.tadris.fitness.fileprovider", new File(file));
GpxExporter.exportWorkout(getBaseContext(), workout, new File(file));
dialogController.cancel();
mHandler.post(() -> shareFile(uri));
mHandler.post(() -> FileUtils.saveOrShareFile(this, uri, "gpx"));
}catch (Exception e){
e.printStackTrace();
mHandler.post(() -> showErrorDialog(e, R.string.error, R.string.errorGpxExportFailed));
}
}).start();
}
private OAuthConsumer oAuthConsumer= null;
private void prepareUpload(){
OAuthAuthentication authentication= new OAuthAuthentication(mHandler, this, new OAuthAuthentication.OAuthAuthenticationListener() {
@Override
public void authenticationFailed() {
new AlertDialog.Builder(ShowWorkoutActivity.this)
.setTitle(R.string.error)
.setMessage(R.string.authenticationFailed)
.setPositiveButton(R.string.okay, null)
.create().show();
}
@Override
public void authenticationComplete(OAuthConsumer consumer) {
oAuthConsumer= consumer;
showUploadOptions();
}
});
authentication.authenticateIfNecessary();
}
private AlertDialog dialog = null;
private void showUploadOptions(){
dialog= new AlertDialog.Builder(this)
.setTitle(R.string.actionUploadToOSM)
.setView(R.layout.dialog_upload_osm)
.setPositiveButton(R.string.upload, (dialogInterface, i) -> {
CheckBox checkBox= dialog.findViewById(R.id.uploadCutting);
Spinner spinner= dialog.findViewById(R.id.uploadVisibility);
EditText descriptionEdit= dialog.findViewById(R.id.uploadDescription);
String description= descriptionEdit.getText().toString().trim();
GpsTraceDetails.Visibility visibility;
switch (spinner.getSelectedItemPosition()){
case 0: visibility= GpsTraceDetails.Visibility.IDENTIFIABLE; break;
default:
case 1: visibility= GpsTraceDetails.Visibility.TRACKABLE; break;
case 2: visibility= GpsTraceDetails.Visibility.PRIVATE; break;
}
uploadToOsm(checkBox.isChecked(), visibility, description);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void uploadToOsm(boolean cut, GpsTraceDetails.Visibility visibility, String description){
List<WorkoutSample> samples = new ArrayList<>(this.samples);
new OsmTraceUploader(this, mHandler, workout, samples, visibility, oAuthConsumer, cut, description).upload();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
@ -223,8 +268,18 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
case R.id.actionExportGpx:
exportToGpx();
return true;
case R.id.actionUploadOSM:
prepareUpload();
return true;
case R.id.actionEditComment:
openEditCommentDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
void initRoot() {
root = findViewById(R.id.showWorkoutRoot);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -32,7 +32,7 @@ public class ShowWorkoutMapActivity extends WorkoutActivity {
initBeforeContent();
setContentView(R.layout.activity_show_workout_map);
root= findViewById(R.id.showWorkoutMapParent);
initRoot();
initAfterContent();
@ -43,4 +43,8 @@ public class ShowWorkoutMapActivity extends WorkoutActivity {
}
@Override
void initRoot() {
root = findViewById(R.id.showWorkoutMapParent);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -36,7 +36,7 @@ public class ShowWorkoutMapDiagramActivity extends WorkoutActivity {
initBeforeContent();
setContentView(R.layout.activity_show_workout_map_diagram);
root= findViewById(R.id.showWorkoutMapParent);
initRoot();
initAfterContent();
@ -54,4 +54,8 @@ public class ShowWorkoutMapDiagramActivity extends WorkoutActivity {
}
@Override
void initRoot() {
root = findViewById(R.id.showWorkoutMapParent);
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 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.ActionBar;
import android.app.AlertDialog;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.NumberPicker;
import de.tadris.fitness.R;
import de.tadris.fitness.util.unit.UnitUtils;
public class VoiceAnnouncementsSettingsActivity extends FitoTrackSettingsActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActionBar();
setTitle(R.string.voiceAnnouncementsTitle);
addPreferencesFromResource(R.xml.preferences_voice_announcements);
bindPreferenceSummaryToValue(findPreference("announcementMode"));
findPreference("speechConfig").setOnPreferenceClickListener(preference -> {
showSpeechConfig();
return true;
});
}
private void showSpeechConfig() {
UnitUtils.setUnit(this); // Maybe the user changed unit system
final AlertDialog.Builder d = new AlertDialog.Builder(this);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
d.setTitle(getString(R.string.pref_voice_announcements_summary));
View v = getLayoutInflater().inflate(R.layout.dialog_spoken_updates_picker, null);
NumberPicker npT = v.findViewById(R.id.spokenUpdatesTimePicker);
npT.setMaxValue(60);
npT.setMinValue(0);
npT.setFormatter(value -> value == 0 ? "No speech" : value + " min");
final String updateTimeVariable = "spokenUpdateTimePeriod";
npT.setValue(preferences.getInt(updateTimeVariable, 0));
npT.setWrapSelectorWheel(false);
final String distanceUnit = " " + UnitUtils.CHOSEN_SYSTEM.getLongDistanceUnit();
NumberPicker npD = v.findViewById(R.id.spokenUpdatesDistancePicker);
npD.setMaxValue(10);
npD.setMinValue(0);
npD.setFormatter(value -> value == 0 ? "No speech" : value + distanceUnit);
final String updateDistanceVariable = "spokenUpdateDistancePeriod";
npD.setValue(preferences.getInt(updateDistanceVariable, 0));
npD.setWrapSelectorWheel(false);
d.setView(v);
d.setNegativeButton(R.string.cancel, null);
d.setPositiveButton(R.string.okay, (dialog, which) ->
preferences.edit()
.putInt(updateTimeVariable, npT.getValue())
.putInt(updateDistanceVariable, npD.getValue())
.apply());
d.create().show();
}
/**
* Set up the {@link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -36,6 +36,7 @@ import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.core.util.LatLongUtils;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
@ -54,46 +55,45 @@ 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.map.tilesource.TileSources;
import de.tadris.fitness.util.ThemeManager;
import de.tadris.fitness.util.WorkoutTypeCalculator;
import de.tadris.fitness.util.unit.UnitUtils;
public abstract class WorkoutActivity extends FitoTrackActivity {
public abstract class WorkoutActivity extends InformationActivity {
public static Workout selectedWorkout;
protected List<WorkoutSample> samples;
protected Workout workout;
protected ViewGroup root;
protected Resources.Theme theme;
protected MapView map;
protected TileDownloadLayer downloadLayer;
protected FixedPixelCircle highlightingCircle;
protected Handler mHandler= new Handler();
List<WorkoutSample> samples;
Workout workout;
private Resources.Theme theme;
MapView map;
private TileDownloadLayer downloadLayer;
private FixedPixelCircle highlightingCircle;
final Handler mHandler = new Handler();
protected LineChart speedDiagram, heightDiagram;
LineChart speedDiagram;
LineChart heightDiagram;
protected void initBeforeContent(){
void initBeforeContent() {
workout= selectedWorkout;
samples= Arrays.asList(Instance.getInstance(this).db.workoutDao().getAllSamplesOfWorkout(workout.id));
setTheme(ThemeManager.getThemeByWorkout(workout));
setTheme(Instance.getInstance(this).themes.getWorkoutTypeTheme(workout.getWorkoutType()));
}
protected void initAfterContent(){
getActionBar().setDisplayHomeAsUpEnabled(true);
setTitle(WorkoutTypeCalculator.getType(workout));
void initAfterContent() {
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
setTitle(workout.getWorkoutType().title);
theme= getTheme();
}
void addDiagram(SampleConverter converter){
private void addDiagram(SampleConverter converter) {
root.addView(getDiagram(converter), new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, fullScreenItems ? ViewGroup.LayoutParams.MATCH_PARENT : getWindowManager().getDefaultDisplay().getWidth()*3/4));
}
protected boolean diagramsInteractive= false;
boolean diagramsInteractive = false;
LineChart getDiagram(SampleConverter converter){
private LineChart getDiagram(SampleConverter converter) {
LineChart chart= new LineChart(this);
converter.onCreate();
@ -111,7 +111,7 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
dataSet.setValueTextColor(getThemePrimaryColor());
dataSet.setDrawCircles(false);
dataSet.setLineWidth(4);
dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
Description description= new Description();
description.setText(converter.getDescription());
@ -126,10 +126,7 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
@Override
public void onValueSelected(Entry e, Highlight h) {
onNothingSelected();
Paint p= AndroidGraphicFactory.INSTANCE.createPaint();
p.setColor(0xff693cff);
highlightingCircle= new FixedPixelCircle(findSample(converter, e).toLatLong(), 20, p, null);
map.addLayer(highlightingCircle);
onDiagramValueSelected(findSample(converter, e).toLatLong());
}
@Override
@ -147,6 +144,17 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
return chart;
}
private void onDiagramValueSelected(LatLong latLong) {
Paint p= AndroidGraphicFactory.INSTANCE.createPaint();
p.setColor(0xff693cff);
highlightingCircle= new FixedPixelCircle(latLong, 20, p, null);
map.addLayer(highlightingCircle);
if(!map.getBoundingBox().contains(latLong)){
map.getModel().mapViewPosition.animateTo(latLong);
}
}
interface SampleConverter{
void onCreate();
float getValue(WorkoutSample sample);
@ -157,7 +165,7 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
void afterAdd(LineChart chart);
}
WorkoutSample findSample(SampleConverter converter, Entry entry){
private WorkoutSample findSample(SampleConverter converter, Entry entry) {
for(WorkoutSample sample : samples){
if(converter.compare(sample, entry)){
return sample;
@ -242,12 +250,12 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
});
}
protected boolean fullScreenItems = false;
protected LinearLayout mapRoot;
boolean fullScreenItems = false;
LinearLayout mapRoot;
void addMap(){
map= new MapView(this);
downloadLayer= MapManager.setupMap(map, TileSources.Purpose.DEFAULT);
downloadLayer = MapManager.setupMap(map);
WorkoutLayer workoutLayer= new WorkoutLayer(samples, getThemePrimaryColor());
map.addLayer(workoutLayer);
@ -281,13 +289,19 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
}
int getMapHeight(){
private int getMapHeight() {
return getWindowManager().getDefaultDisplay().getWidth()*3/4;
}
protected boolean hasSamples() {
return samples.size() > 1;
}
@Override
protected void onDestroy() {
map.destroyAll();
if (map != null) {
map.destroyAll();
}
AndroidGraphicFactory.clearResourceMemoryCache();
super.onDestroy();
}
@ -295,12 +309,16 @@ public abstract class WorkoutActivity extends FitoTrackActivity {
@Override
public void onPause(){
super.onPause();
downloadLayer.onPause();
if (downloadLayer != null) {
downloadLayer.onPause();
}
}
public void onResume(){
super.onResume();
downloadLayer.onResume();
if (downloadLayer != null) {
downloadLayer.onResume();
}
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -22,7 +22,7 @@ package de.tadris.fitness.data;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(version = 2, entities = {Workout.class, WorkoutSample.class})
@Database(version = 3, entities = {Workout.class, WorkoutSample.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract WorkoutDao workoutDao();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -25,7 +25,7 @@ import android.preference.PreferenceManager;
public class UserPreferences {
private SharedPreferences preferences;
private final SharedPreferences preferences;
public UserPreferences(Context context) {
this.preferences= PreferenceManager.getDefaultSharedPreferences(context);
@ -35,6 +35,14 @@ public class UserPreferences {
return preferences.getInt("weight", 80);
}
public int getSpokenUpdateTimePeriod(){
return preferences.getInt("spokenUpdateTimePeriod", 0);
}
public int getSpokenUpdateDistancePeriod(){
return preferences.getInt("spokenUpdateDistancePeriod", 0);
}
public String getMapStyle(){
return preferences.getString("mapStyle", "osm.mapnik");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,10 +19,13 @@
package de.tadris.fitness.data;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -31,10 +34,6 @@ import java.util.Date;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Workout{
public static final String WORKOUT_TYPE_RUNNING= "running";
public static final String WORKOUT_TYPE_HIKING= "hiking";
public static final String WORKOUT_TYPE_CYCLING= "cycling";
@PrimaryKey
public long id;
@ -63,12 +62,13 @@ public class Workout{
public double topSpeed;
/**
* Average pace of workout in km/min
* Average pace of workout in min/km
*/
public double avgPace;
public String workoutType;
@ColumnInfo(name = "workoutType")
@JsonProperty(value = "workoutType")
public String workoutTypeId;
public float ascent;
@ -76,13 +76,30 @@ public class Workout{
public int calorie;
public boolean edited;
public String toString(){
if(comment.length() > 2){
return comment;
}else{
return SimpleDateFormat.getDateTimeInstance().format(new Date(start));
return getDateString();
}
}
@JsonIgnore
public String getDateString(){
return SimpleDateFormat.getDateTimeInstance().format(new Date(start));
}
@JsonIgnore
public WorkoutType getWorkoutType() {
return WorkoutType.getTypeById(workoutTypeId);
}
@JsonIgnore
public void setWorkoutType(WorkoutType workoutType) {
this.workoutTypeId = workoutType.id;
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2020 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.data;
import android.content.Context;
import java.util.Calendar;
import de.tadris.fitness.Instance;
import de.tadris.fitness.util.CalorieCalculator;
public class WorkoutBuilder {
private WorkoutType workoutType;
private Calendar start;
private long duration;
private int length;
private String comment;
public WorkoutBuilder() {
workoutType = WorkoutType.RUNNING;
start = Calendar.getInstance();
duration = 1000L * 60 * 10;
length = 500;
comment = "";
}
public Workout create(Context context) {
Workout workout = new Workout();
// Calculate values
workout.start = start.getTimeInMillis();
workout.duration = duration;
workout.end = workout.start + workout.duration;
workout.id = workout.start;
workout.setWorkoutType(workoutType);
workout.length = length;
workout.avgSpeed = (double) length / (double) (duration / 1000);
workout.topSpeed = 0;
workout.avgPace = ((double) workout.duration / 1000 / 60) / ((double) workout.length / 1000);
workout.pauseDuration = 0;
workout.ascent = 0;
workout.descent = 0;
workout.calorie = CalorieCalculator.calculateCalories(workout, Instance.getInstance(context).userPreferences.getUserWeight());
workout.comment = comment;
workout.edited = true;
return workout;
}
public Workout insertWorkout(Context context) {
Workout workout = create(context);
Instance.getInstance(context).db.workoutDao().insertWorkout(workout);
return workout;
}
public WorkoutType getWorkoutType() {
return workoutType;
}
public void setWorkoutType(WorkoutType workoutType) {
this.workoutType = workoutType;
}
public Calendar getStart() {
return start;
}
public void setStart(Calendar start) {
this.start = start;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,118 +19,10 @@
package de.tadris.fitness.data;
import android.content.Context;
import android.hardware.SensorManager;
import android.util.Log;
import java.util.List;
import de.tadris.fitness.Instance;
import de.tadris.fitness.util.CalorieCalculator;
public class WorkoutManager {
public static void insertWorkout(Context context, Workout workout, List<WorkoutSample> samples){
AppDatabase db= Instance.getInstance(context).db;
workout.id= System.currentTimeMillis();
// Delete Samples with same time
for(int i= samples.size()-2; i >= 0; i--){
WorkoutSample sample= samples.get(i);
WorkoutSample lastSample= samples.get(i+1);
if(sample.absoluteTime == lastSample.absoluteTime){
samples.remove(lastSample);
Log.i("WorkoutManager", "Removed samples at " + sample.absoluteTime + " rel: " + sample.relativeTime + "; " + lastSample.relativeTime);
}
}
// Calculating values
double length= 0;
for(int i= 1; i < samples.size(); i++){
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.duration / 1000);
workout.avgPace= ((double)workout.duration / 1000 / 60) / ((double) workout.length / 1000);
workout.calorie= CalorieCalculator.calculateCalories(workout, Instance.getInstance(context).userPreferences.getUserWeight());
// Setting workoutId in the samples
int i= 0;
double topSpeed= 0;
double elevationSum= 0; // Sum of elevation
double pressureSum= 0; // Sum of elevation
for(WorkoutSample sample : samples){
i++;
sample.id= workout.id + i;
sample.workoutId= workout.id;
elevationSum+= sample.elevation;
pressureSum+= sample.tmpPressure;
if(sample.speed > topSpeed){
topSpeed= sample.speed;
}
}
workout.topSpeed= topSpeed;
// Calculating height data
boolean pressureDataAvailable= samples.get(0).tmpPressure != -1;
double avgElevation= elevationSum / samples.size();
double avgPressure= pressureSum / samples.size();
workout.ascent = 0;
workout.descent = 0;
for(i= 0; i < samples.size(); i++){
WorkoutSample sample= samples.get(i);
if(pressureDataAvailable){
// Altitude Difference to Average Elevation in meters
float altitude_difference =
SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, sample.tmpPressure) -
SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, (float) avgPressure);
sample.elevation= avgElevation + altitude_difference;
} // Else: use already set GPS elevation in WorkoutSample.elevation
}
int range= 3;
for(i= 0; i < samples.size(); i++){
int min= Math.max(i-range, 0);
int max= Math.min(i+range, samples.size()-1);
samples.get(i).tmpElevation= getAverageElevation(samples.subList(min, max));
}
for(i= 0; i < samples.size(); i++) {
WorkoutSample sample = samples.get(i);
sample.elevation= sample.tmpElevation;
if(i >= 1){
WorkoutSample lastSample= samples.get(i-1);
double diff= sample.elevation - lastSample.elevation;
if(diff > 0){
workout.ascent += diff;
}else{
workout.descent += Math.abs(diff);
}
}
}
// Saving workout and samples
db.workoutDao().insertWorkoutAndSamples(workout, samples.toArray(new WorkoutSample[0]));
}
public static double getAverageElevation(List<WorkoutSample> samples){
double sum= 0;
for(WorkoutSample sample : samples){
sum+= sample.elevation;
}
return sum / samples.size();
}
public static void roundSpeedValues(List<WorkoutSample> samples){
for(int i= 0; i < samples.size(); i++){
WorkoutSample sample= samples.get(i);

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 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.data;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import de.tadris.fitness.R;
public enum WorkoutType {
RUNNING("running", R.string.workoutTypeRunning, 7, true, R.style.Running, R.style.RunningDark),
HIKING("hiking", R.string.workoutTypeHiking, 7, true, R.style.Hiking, R.style.HikingDark),
CYCLING("cycling", R.string.workoutTypeCycling, 12, true, R.style.Bicycling, R.style.BicyclingDark),
OTHER("other", R.string.workoutTypeOther, 7, true, R.style.AppTheme, R.style.AppThemeDark);
public String id;
@StringRes
public int title;
public int minDistance; // Minimum distance between samples
public boolean hasGPS;
@StyleRes
public int lightTheme, darkTheme;
WorkoutType(String id, int title, int minDistance, boolean hasGPS, int lightTheme, int darkTheme) {
this.id = id;
this.title = title;
this.minDistance = minDistance;
this.hasGPS = hasGPS;
this.lightTheme = lightTheme;
this.darkTheme = darkTheme;
}
public static WorkoutType getTypeById(String id) {
for (WorkoutType type : values()) {
if (type.id.equals(id)) {
return type;
}
}
return OTHER;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 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.dialog;
import android.app.DatePickerDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.os.Bundle;
import android.widget.DatePicker;
import java.util.Calendar;
public class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener {
public DatePickerCallback callback;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current date as the default date in the picker
final Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
// Create a new instance of DatePickerDialog and return it
return new DatePickerDialog(getActivity(), this, year, month, day);
}
public void onDateSet(DatePicker view, int year, int month, int day) {
callback.onDatePick(year, month, day);
}
public interface DatePickerCallback {
void onDatePick(int year, int month, int day);
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2020 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.dialog;
import android.app.Activity;
import android.app.AlertDialog;
import android.view.View;
import android.widget.NumberPicker;
import de.tadris.fitness.R;
public class DurationPickerDialogFragment {
public Activity context;
public DurationPickListener listener;
public long initialDuration;
public DurationPickerDialogFragment(Activity context, DurationPickListener listener, long initialDuration) {
this.context = context;
this.listener = listener;
this.initialDuration = initialDuration;
}
public void show() {
final AlertDialog.Builder d = new AlertDialog.Builder(context);
d.setTitle(R.string.setDuration);
View v = context.getLayoutInflater().inflate(R.layout.dialog_duration_picker, null);
NumberPicker hours = v.findViewById(R.id.durationPickerHours);
hours.setFormatter(value -> value + " " + context.getString(R.string.timeHourShort));
hours.setMinValue(0);
hours.setMaxValue(24);
hours.setValue(getInitialHours());
NumberPicker minutes = v.findViewById(R.id.durationPickerMinutes);
minutes.setFormatter(value -> value + " " + context.getString(R.string.timeMinuteShort));
minutes.setMinValue(0);
minutes.setMaxValue(60);
minutes.setValue(getInitialMinutes());
NumberPicker seconds = v.findViewById(R.id.durationPickerSeconds);
seconds.setFormatter(value -> value + " " + context.getString(R.string.timeSecondsShort));
seconds.setMinValue(0);
seconds.setMaxValue(60);
seconds.setValue(getInitialSeconds());
d.setView(v);
d.setNegativeButton(R.string.cancel, null);
d.setPositiveButton(R.string.okay, (dialog, which) -> {
listener.onDurationPick(getMillisFromPick(hours.getValue(), minutes.getValue(), seconds.getValue()));
});
d.create().show();
}
private int getInitialHours() {
return (int) (initialDuration / 1000 / 60 / 60);
}
private int getInitialMinutes() {
return (int) (initialDuration / 1000 / 60 % 60);
}
private int getInitialSeconds() {
return (int) (initialDuration / 1000 % 60);
}
private static long getMillisFromPick(int hours, int minutes, int seconds) {
long secondInMillis = 1000L;
long minuteInMillis = secondInMillis * 60;
long hourInMillis = minuteInMillis * 60;
return hours * hourInMillis + minutes * minuteInMillis + seconds * secondInMillis;
}
public interface DurationPickListener {
void onDurationPick(long duration);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 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.dialog;
import android.app.Activity;
import android.app.AlertDialog;
import android.widget.ArrayAdapter;
import de.tadris.fitness.R;
import de.tadris.fitness.data.WorkoutType;
public class SelectWorkoutTypeDialog {
private Activity context;
private WorkoutTypeSelectListener listener;
private WorkoutType[] options;
public SelectWorkoutTypeDialog(Activity context, WorkoutTypeSelectListener listener) {
this.context = context;
this.listener = listener;
this.options = WorkoutType.values();
}
public void show() {
AlertDialog.Builder builderSingle = new AlertDialog.Builder(context);
final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(context, R.layout.select_dialog_singlechoice_material);
for (WorkoutType type : options) {
arrayAdapter.add(context.getString(type.title));
}
builderSingle.setAdapter(arrayAdapter, (dialog, which) -> listener.onSelectWorkoutType(options[which]));
builderSingle.show();
}
public interface WorkoutTypeSelectListener {
void onSelectWorkoutType(WorkoutType workoutType);
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020 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.dialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.TimePickerDialog;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.widget.TimePicker;
import java.util.Calendar;
public class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener {
public TimePickerCallback callback;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current time as the default values for the picker
final Calendar c = Calendar.getInstance();
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
// Create a new instance of TimePickerDialog and return it
return new TimePickerDialog(getActivity(), this, hour, minute, DateFormat.is24HourFormat(getActivity()));
}
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
callback.onTimePick(hourOfDay, minute);
}
public interface TimePickerCallback {
void onTimePick(int hour, int minute);
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2020 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.export;
import android.content.Context;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.util.unit.UnitUtils;
public class BackupController {
private static final int VERSION = 1;
private final Context context;
private final File output;
private final ExportStatusListener listener;
private AppDatabase database;
private FitoTrackDataContainer dataContainer;
public BackupController(Context context, File output, ExportStatusListener listener) {
this.context = context;
this.output = output;
this.listener = listener;
}
public void exportData() throws IOException {
listener.onStatusChanged(0, context.getString(R.string.initialising));
init();
listener.onStatusChanged(20, context.getString(R.string.workouts));
saveWorkoutsToContainer();
listener.onStatusChanged(40, context.getString(R.string.locationData));
saveSamplesToContainer();
listener.onStatusChanged(60, context.getString(R.string.converting));
writeContainerToOutputFile();
listener.onStatusChanged(100, context.getString(R.string.finished));
}
private void init(){
database= Instance.getInstance(context).db;
UnitUtils.setUnit(context); // Ensure unit system is correct
newContainer();
}
private void newContainer(){
dataContainer= new FitoTrackDataContainer();
dataContainer.setVersion(VERSION);
dataContainer.setWorkouts(new ArrayList<>());
dataContainer.setSamples(new ArrayList<>());
}
private void saveWorkoutsToContainer(){
dataContainer.getWorkouts().addAll(Arrays.asList(database.workoutDao().getWorkouts()));
}
private void saveSamplesToContainer(){
dataContainer.getSamples().addAll(Arrays.asList(database.workoutDao().getSamples()));
}
private void writeContainerToOutputFile() throws IOException {
XmlMapper mapper= new XmlMapper();
mapper.writeValue(output, dataContainer);
}
public interface ExportStatusListener{
void onStatusChanged(int progress, String action);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -17,7 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.util.export;
package de.tadris.fitness.export;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@ -29,20 +29,18 @@ import de.tadris.fitness.data.WorkoutSample;
@JacksonXmlRootElement(localName = "fito-track")
@JsonIgnoreProperties(ignoreUnknown = true)
public class FitoTrackDataContainer {
class FitoTrackDataContainer {
int version;
List<Workout> workouts;
List<WorkoutSample> samples;
FitoTrackSettings settings;
private int version;
private List<Workout> workouts;
private List<WorkoutSample> samples;
public FitoTrackDataContainer(){}
public FitoTrackDataContainer(int version, List<Workout> workouts, List<WorkoutSample> samples, FitoTrackSettings settings) {
public FitoTrackDataContainer(int version, List<Workout> workouts, List<WorkoutSample> samples) {
this.version = version;
this.workouts = workouts;
this.samples = samples;
this.settings = settings;
}
public int getVersion() {
@ -69,11 +67,5 @@ public class FitoTrackDataContainer {
this.samples = samples;
}
public FitoTrackSettings getSettings() {
return settings;
}
public void setSettings(FitoTrackSettings settings) {
this.settings = settings;
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2020 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.export;
import android.content.Context;
import android.net.Uri;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.IOException;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
public class RestoreController {
private final Context context;
private final Uri input;
private final ImportStatusListener listener;
private FitoTrackDataContainer dataContainer;
private final AppDatabase database;
public RestoreController(Context context, Uri input, ImportStatusListener listener) {
this.context = context;
this.input = input;
this.listener = listener;
this.database= Instance.getInstance(context).db;
}
public void restoreData() throws IOException, UnsupportedVersionException{
listener.onStatusChanged(0, context.getString(R.string.loadingFile));
loadDataFromFile();
checkVersion();
restoreDatabase();
listener.onStatusChanged(100, context.getString(R.string.finished));
}
private void loadDataFromFile() throws IOException {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.configure(JsonParser.Feature.IGNORE_UNDEFINED, true);
dataContainer = xmlMapper.readValue(context.getContentResolver().openInputStream(input), FitoTrackDataContainer.class);
}
private void checkVersion() throws UnsupportedVersionException {
if (dataContainer.getVersion() != 1) {
throw new UnsupportedVersionException("Version Code" + dataContainer.getVersion() + " is unsupported!");
}
}
private void restoreDatabase(){
database.runInTransaction(() -> {
resetDatabase();
restoreWorkouts();
restoreSamples();
});
}
private void resetDatabase(){
database.clearAllTables();
}
private void restoreWorkouts(){
listener.onStatusChanged(60, context.getString(R.string.workouts));
if (dataContainer.getWorkouts() != null) {
for (Workout workout : dataContainer.getWorkouts()) {
database.workoutDao().insertWorkout(workout);
}
}
}
private void restoreSamples(){
listener.onStatusChanged(80, context.getString(R.string.locationData));
if (dataContainer.getSamples() != null) {
for (WorkoutSample sample : dataContainer.getSamples()) {
database.workoutDao().insertSample(sample);
}
}
}
public interface ImportStatusListener{
void onStatusChanged(int progress, String action);
}
static class UnsupportedVersionException extends Exception {
UnsupportedVersionException(String message) {
super(message);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,7 +19,6 @@
package de.tadris.fitness.map;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
@ -31,18 +30,19 @@ import de.tadris.fitness.map.tilesource.FitoTrackTileSource;
import de.tadris.fitness.map.tilesource.HumanitarianTileSource;
import de.tadris.fitness.map.tilesource.MapnikTileSource;
import de.tadris.fitness.map.tilesource.ThunderforestTileSource;
import de.tadris.fitness.map.tilesource.TileSources;
public class MapManager {
public static TileDownloadLayer setupMap(MapView mapView, TileSources.Purpose purpose){
public static TileDownloadLayer setupMap(MapView mapView) {
FitoTrackTileSource tileSource;
String chosenTileLayer= Instance.getInstance(mapView.getContext()).userPreferences.getMapStyle();
switch (chosenTileLayer){
case "osm.humanitarian": tileSource= HumanitarianTileSource.INSTANCE; break;
case "thunderforest.outdoors": tileSource= ThunderforestTileSource.OUTDOORS; break;
case "thunderforest.cycle": tileSource= ThunderforestTileSource.CYLE_MAP; break;
case "thunderforest.cycle":
tileSource = ThunderforestTileSource.CYCLE_MAP;
break;
default: tileSource= MapnikTileSource.INSTANCE; break; // Inclusive "osm.mapnik"
}
tileSource.setUserAgent("mapsforge-android");
@ -62,7 +62,6 @@ public class MapManager {
mapView.getLayerManager().redrawLayers();
mapView.setZoomLevel((byte) 18);
mapView.setCenter(new LatLong(52, 13));
return downloadLayer;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -30,7 +30,7 @@ import de.tadris.fitness.data.WorkoutSample;
public class WorkoutLayer extends Polyline {
public static Paint getDEFAULT_PAINT_STROKE(int color){
private static Paint getDEFAULT_PAINT_STROKE(int color) {
Paint paint= AndroidGraphicFactory.INSTANCE.createPaint();
paint.setStyle(Style.STROKE);
paint.setColor(color);
@ -38,13 +38,13 @@ public class WorkoutLayer extends Polyline {
return paint;
}
List<WorkoutSample> samples;
private final List<WorkoutSample> samples;
public WorkoutLayer(List<WorkoutSample> samples, int color) {
this(getDEFAULT_PAINT_STROKE(color), samples);
}
public WorkoutLayer(Paint paintStroke, List<WorkoutSample> samples) {
private WorkoutLayer(Paint paintStroke, List<WorkoutSample> samples) {
super(paintStroke, AndroidGraphicFactory.INSTANCE);
this.samples = samples;
init();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -23,7 +23,7 @@ import org.mapsforge.map.layer.download.tilesource.AbstractTileSource;
public abstract class FitoTrackTileSource extends AbstractTileSource {
public FitoTrackTileSource(String[] hostNames, int port) {
FitoTrackTileSource(String[] hostNames, int port) {
super(hostNames, port);
defaultTimeToLive = 8279000;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -26,7 +26,7 @@ import java.net.URL;
public class HumanitarianTileSource extends FitoTrackTileSource {
public static HumanitarianTileSource INSTANCE= new HumanitarianTileSource(new String[]{"tile-a.openstreetmap.fr", "tile-b.openstreetmap.fr", "tile-c.openstreetmap.fr"}, 443);
public static final HumanitarianTileSource INSTANCE = new HumanitarianTileSource(new String[]{"tile-a.openstreetmap.fr", "tile-b.openstreetmap.fr", "tile-c.openstreetmap.fr"}, 443);
private static final int PARALLEL_REQUESTS_LIMIT = 8;
private static final String PROTOCOL = "https";
@ -34,7 +34,7 @@ public class HumanitarianTileSource extends FitoTrackTileSource {
private static final int ZOOM_LEVEL_MIN = 0;
private static final String NAME = "Humanitarian";
public HumanitarianTileSource(String[] hostNames, int port) {
private HumanitarianTileSource(String[] hostNames, int port) {
super(hostNames, port);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -30,11 +30,11 @@ public class MapnikTileSource extends FitoTrackTileSource {
"a.tile.openstreetmap.org", "b.tile.openstreetmap.org", "c.tile.openstreetmap.org"}, 443);
private static final int PARALLEL_REQUESTS_LIMIT = 8;
private static final String PROTOCOL = "https";
private static final int ZOOM_LEVEL_MAX = 18;
private static final int ZOOM_LEVEL_MAX = 19;
private static final int ZOOM_LEVEL_MIN = 0;
private static final String NAME = "OSM Mapnik";
public MapnikTileSource(String[] hostNames, int port) {
private MapnikTileSource(String[] hostNames, int port) {
super(hostNames, port);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -26,19 +26,19 @@ import java.net.URL;
public class ThunderforestTileSource extends FitoTrackTileSource{
public static final String API_KEY= "87b07337e42c405db6d8d39b1c0c179e";
private static final String API_KEY = "87b07337e42c405db6d8d39b1c0c179e";
public static final ThunderforestTileSource OUTDOORS = new ThunderforestTileSource("outdoors", "Outdoor");
public static final ThunderforestTileSource CYLE_MAP = new ThunderforestTileSource("cycle", "Cycle Map");
public static final ThunderforestTileSource CYCLE_MAP = new ThunderforestTileSource("cycle", "Cycle Map");
private static final int PARALLEL_REQUESTS_LIMIT = 8;
private static final String PROTOCOL = "https";
private static final int ZOOM_LEVEL_MAX = 22;
private static final int ZOOM_LEVEL_MAX = 19;
private static final int ZOOM_LEVEL_MIN = 0;
private String mapName;
private String name;
private final String mapName;
private final String name;
public ThunderforestTileSource(String mapName, String name) {
private ThunderforestTileSource(String mapName, String name) {
super(new String[]{"tile.thunderforest.com"}, 443);
this.mapName = mapName;
this.name = name;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -17,25 +17,32 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.util;
package de.tadris.fitness.osm;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
import de.westnordost.osmapi.map.data.LatLon;
public class ThemeManager {
class GpsTraceLatLong implements LatLon {
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;
case Workout.WORKOUT_TYPE_HIKING: return R.style.Hiking;
default: return R.style.AppTheme;
}
private final double latitude;
private final double longitude;
private GpsTraceLatLong(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public static int getThemeByWorkout(Workout workout){
return getThemeByWorkoutType(workout.workoutType);
public GpsTraceLatLong(WorkoutSample sample) {
this(sample.lat, sample.lon);
}
@Override
public double getLatitude() {
return latitude;
}
@Override
public double getLongitude() {
return longitude;
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) 2020 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.osm;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.provider.Browser;
import android.text.InputType;
import android.widget.EditText;
import de.tadris.fitness.R;
import de.tadris.fitness.view.ProgressDialogController;
import oauth.signpost.OAuth;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.OAuthProvider;
import oauth.signpost.exception.OAuthException;
public class OAuthAuthentication {
private final OAuthConsumer oAuthConsumer = OAuthUrlProvider.getDefaultConsumer();
private final OAuthProvider oAuthProvider = OAuthUrlProvider.getDefaultProvider();
private final Handler handler;
private final Activity activity;
private final ProgressDialogController dialogController;
private final SharedPreferences preferences;
private final OAuthAuthenticationListener listener;
public OAuthAuthentication(Handler handler, Activity activity, OAuthAuthenticationListener listener) {
this.handler = handler;
this.activity = activity;
dialogController= new ProgressDialogController(activity, activity.getString(R.string.uploading));
this.preferences= activity.getSharedPreferences("osm_oauth", Context.MODE_PRIVATE);
this.listener= listener;
}
public void authenticateIfNecessary(){
if(isAuthenticated()){
loadAccessToken();
listener.authenticationComplete(oAuthConsumer);
}else{
retrieveRequestToken();
}
}
private boolean isAuthenticated(){
return preferences.getBoolean("authenticated", false);
}
private void retrieveRequestToken(){
dialogController.show();
dialogController.setIndeterminate(true);
new Thread(() -> {
try {
String authUrl = oAuthProvider.retrieveRequestToken(oAuthConsumer, OAuth.OUT_OF_BAND);
handler.post(() -> {
Intent intent= new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl));
intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_SINGLE_TOP);
activity.startActivity(intent);
showEnterVerificationCodeDialog();
dialogController.cancel();
});
} catch (OAuthException e) {
e.printStackTrace();
handler.post(() -> {
dialogController.cancel();
listener.authenticationFailed();
});
}
}).start();
}
private void showEnterVerificationCodeDialog(){
EditText editText= new EditText(activity);
editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setSingleLine(true);
AlertDialog dialog= new AlertDialog.Builder(activity)
.setTitle(R.string.enterVerificationCode).setView(editText).setPositiveButton(R.string.okay, (dialogInterface, i) -> {
new Thread(() -> retrieveAccessToken(editText.getText().toString().trim())).start();
dialogInterface.cancel();
}).setNegativeButton(R.string.cancel, null).setCancelable(false).create();
dialog.show();
}
private void loadAccessToken(){
oAuthConsumer.setTokenWithSecret(preferences.getString("accessToken", ""),
preferences.getString("tokenSecret", ""));
}
private void saveAccessToken(){
preferences.edit()
.putString("accessToken", oAuthConsumer.getToken())
.putString("tokenSecret", oAuthConsumer.getTokenSecret())
.putBoolean("authenticated", true).apply();
}
void clearAccessToken() {
preferences.edit().putBoolean("authenticated", false).apply();
}
private void retrieveAccessToken(String verificationCode){
handler.post(dialogController::show);
try{
oAuthProvider.retrieveAccessToken(oAuthConsumer, verificationCode);
handler.post(() -> {
dialogController.cancel();
saveAccessToken();
listener.authenticationComplete(oAuthConsumer);
});
}catch (OAuthException e){
handler.post(() -> {
dialogController.cancel();
listener.authenticationFailed();
});
e.printStackTrace();
}
}
public interface OAuthAuthenticationListener{
void authenticationFailed();
void authenticationComplete(OAuthConsumer consumer);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 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.osm;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.OAuthProvider;
import oauth.signpost.basic.DefaultOAuthConsumer;
import oauth.signpost.basic.DefaultOAuthProvider;
class OAuthUrlProvider {
static private final String CONSUMER_KEY= "jFL9grFmAo5ZS720YDDRXdSOb7F0IZQf9lnY1PHq";
static private final String CONSUMER_SECRET= "oH969vYW60fZLco6E09UQl3uFXqjl4siQbOL0q9q";
static OAuthConsumer getDefaultConsumer(){
return new DefaultOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
}
static OAuthProvider getDefaultProvider(){
return new DefaultOAuthProvider(URL_TOKEN_REQUEST, URL_TOKEN_ACCESS, URL_AUTHORIZE);
}
static private final String URL_TOKEN_REQUEST= "https://www.openstreetmap.org/oauth/request_token";
static private final String URL_TOKEN_ACCESS= "https://www.openstreetmap.org/oauth/access_token";
static private final String URL_AUTHORIZE= "https://www.openstreetmap.org/oauth/authorize";
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (c) 2020 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.osm;
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.StringRes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.view.ProgressDialogController;
import de.westnordost.osmapi.OsmConnection;
import de.westnordost.osmapi.common.errors.OsmAuthorizationException;
import de.westnordost.osmapi.traces.GpsTraceDetails;
import de.westnordost.osmapi.traces.GpsTracesDao;
import de.westnordost.osmapi.traces.GpsTrackpoint;
import oauth.signpost.OAuthConsumer;
public class OsmTraceUploader {
private static final int CUT_DISTANCE= 300;
private final Activity activity;
private final Handler handler;
private final Workout workout;
private final List<WorkoutSample> samples;
private final GpsTraceDetails.Visibility visibility;
private final OAuthConsumer consumer;
private final boolean cut;
private final ProgressDialogController dialogController;
private final String description;
public OsmTraceUploader(Activity activity, Handler handler, Workout workout, List<WorkoutSample> samples, GpsTraceDetails.Visibility visibility, OAuthConsumer consumer, boolean cut, String description) {
this.activity = activity;
this.handler = handler;
this.workout = workout;
this.samples = samples;
this.visibility = visibility;
this.consumer = consumer;
this.cut = cut;
this.description= description;
this.dialogController= new ProgressDialogController(activity, activity.getString(R.string.uploading));
}
private void cut(){
cut(false);
cut(true);
}
private void cut(boolean last){
double distance= 0;
int count= 0;
WorkoutSample lastSample= samples.remove(last ? samples.size()-1 : 0);
while(distance < CUT_DISTANCE){
WorkoutSample currentSample= samples.remove(last ? samples.size()-1 : 0);
distance+= lastSample.toLatLong().sphericalDistance(currentSample.toLatLong());
count++;
lastSample= currentSample;
}
Log.d("Uploader", "Cutted " + (last ? "last" : "first") + " " + count + " Samples (" + distance + " meters)");
}
public void upload(){
new Thread(() -> {
try {
executeTask();
}catch (Exception e){
e.printStackTrace();
handler.post(() -> {
@StringRes int textRes = R.string.uploadFailed;
if (e instanceof OsmAuthorizationException) {
textRes = R.string.uploadFailedOsmNotAuthorized;
new OAuthAuthentication(handler, activity, null).clearAccessToken();
}
Toast.makeText(activity, textRes, Toast.LENGTH_LONG).show();
dialogController.cancel();
});
}
}).start();
}
private void executeTask(){
handler.post(dialogController::show);
setProgress(0);
if(cut){ cut(); }
setProgress(20);
OsmConnection osm = new OsmConnection(
"https://api.openstreetmap.org/api/0.6/", "FitoTrack", consumer);
List<GpsTrackpoint> trackpoints= new ArrayList<>();
for(WorkoutSample sample : samples){
GpsTrackpoint trackpoint= new GpsTrackpoint(new GpsTraceLatLong(sample));
trackpoint.time= new Date(sample.absoluteTime);
trackpoint.elevation= (float)sample.elevation;
trackpoints.add(trackpoint);
}
setProgress(25);
new GpsTracesDao(osm).create(workout.getDateString(), visibility, description, Collections.singletonList("FitoTrack"), trackpoints);
setProgress(100);
handler.post(() -> {
Toast.makeText(activity, R.string.uploadSuccessful, Toast.LENGTH_LONG).show();
dialogController.cancel();
});
}
private void setProgress(int progress){
handler.post(() -> dialogController.setProgress(progress));
}
}

View File

@ -1,6 +1,6 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -51,9 +51,9 @@ public class LocationListener extends Service {
private static final int LOCATION_INTERVAL = 1000;
private class LocationChangedListener implements android.location.LocationListener {
Location mLastLocation;
final Location mLastLocation;
public LocationChangedListener(String provider) {
LocationChangedListener(String provider) {
Log.i(TAG, "LocationListener " + provider);
mLastLocation = new Location(provider);
}
@ -83,7 +83,7 @@ public class LocationListener extends Service {
}
}
LocationChangedListener gpsListener= new LocationChangedListener(LocationManager.GPS_PROVIDER);
private final LocationChangedListener gpsListener = new LocationChangedListener(LocationManager.GPS_PROVIDER);
@Override
public IBinder onBind(Intent arg0) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -31,27 +31,21 @@ import java.util.List;
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.data.WorkoutType;
import de.tadris.fitness.util.CalorieCalculator;
public class WorkoutRecorder implements LocationListener.LocationChangeListener {
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;
/**
* Time after which the workout is stopped and saved automatically because there is no activity anymore
*/
private static final int AUTO_STOP_TIMEOUT= 1000*60*60*20; // 20 minutes
private final Context context;
private final Workout workout;
private RecordingState state;
private final List<WorkoutSample> samples= new ArrayList<>();
private long time= 0;
@ -60,25 +54,26 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
private long lastPause= 0;
private long lastSampleTime= 0;
private double distance= 0;
private boolean hasBegan = false;
private boolean hasBegun = false;
private static final double SIGNAL_BAD_THRESHOLD= 20; // In meters
private static final int SIGNAL_LOST_THRESHOLD= 10000; // In milliseconds
private Location lastFix= null;
private GpsStateChangedListener gpsStateChangedListener;
private final WorkoutRecorderListener workoutRecorderListener;
private GpsState gpsState= GpsState.SIGNAL_LOST;
public WorkoutRecorder(Context context, String workoutType, GpsStateChangedListener gpsStateChangedListener) {
public WorkoutRecorder(Context context, WorkoutType workoutType, WorkoutRecorderListener workoutRecorderListener) {
this.context= context;
this.state= RecordingState.IDLE;
this.gpsStateChangedListener= gpsStateChangedListener;
this.workoutRecorderListener = workoutRecorderListener;
this.workout= new Workout();
workout.edited = false;
// Default values
this.workout.comment= "";
this.workout.workoutType= workoutType;
this.workout.setWorkoutType(workoutType);
}
public void start(){
@ -107,8 +102,15 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
synchronized (samples){
if(samples.size() > 2){
WorkoutSample lastSample= samples.get(samples.size()-1);
if(System.currentTimeMillis() - lastSampleTime > PAUSE_TIME){
if(state == RecordingState.RUNNING){
long timeDiff= System.currentTimeMillis() - lastSampleTime;
if(timeDiff > AUTO_STOP_TIMEOUT){
if(isActive()){
stop();
save();
workoutRecorderListener.onAutoStop();
}
}else if(timeDiff > PAUSE_TIME){
if (state == RecordingState.RUNNING && gpsState != GpsState.SIGNAL_LOST) {
pause();
}
}else{
@ -123,7 +125,7 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}, "WorkoutWatchdog").start();
}
private void checkSignalState(){
@ -139,7 +141,7 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
state= GpsState.SIGNAL_OKAY;
}
if(state != gpsState){
gpsStateChangedListener.onGPSStateChanged(gpsState, state);
workoutRecorderListener.onGPSStateChanged(gpsState, state);
gpsState= state;
}
}
@ -153,7 +155,7 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
}
}
public void pause(){
private void pause() {
if(state == RecordingState.RUNNING){
Log.i("Recorder", "Pause");
state= RecordingState.PAUSED;
@ -181,7 +183,7 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
}
Log.i("Recorder", "Save");
synchronized (samples){
WorkoutManager.insertWorkout(context, workout, samples);
new WorkoutSaver(context, workout, samples).saveWorkout();
}
}
@ -197,53 +199,58 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
if(isActive()){
double distance= 0;
if(getSampleCount() > 0){
// Checks whether the minimum distance to last sample was reached
// and if the time difference to the last sample is too small
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){
if (distance < workout.getWorkoutType().minDistance && timediff < 500) {
return;
}
}
}
lastSampleTime= System.currentTimeMillis();
if(state == RecordingState.RUNNING && location.getTime() > workout.start){
if(samples.size() == 2 && !hasBegan){
lastResume= System.currentTimeMillis();
workout.start= System.currentTimeMillis();
lastPause= 0;
time= 0;
pauseTime= 0;
this.distance= 0;
samples.clear();
hasBegan = true; // Do not clear a second time
if(samples.size() == 2 && !hasBegun){
initialClearValues();
hasBegun = true; // Do not clear a second time
}
this.distance+= distance;
WorkoutSample sample= new WorkoutSample();
sample.lat= location.getLatitude();
sample.lon= location.getLongitude();
sample.elevation= location.getAltitude();
sample.speed= location.getSpeed();
sample.relativeTime= location.getTime() - workout.start - pauseTime;
sample.absoluteTime= location.getTime();
if(Instance.getInstance(context).pressureAvailable){
sample.tmpPressure= Instance.getInstance(context).lastPressure;
}else{
sample.tmpPressure= -1;
}
synchronized (samples){
samples.add(sample);
}
addToSamples(location);
}
}
}
/**
* Returns the distance in meters
*/
public int getDistance(){
private void addToSamples(Location location){
WorkoutSample sample= new WorkoutSample();
sample.lat= location.getLatitude();
sample.lon= location.getLongitude();
sample.elevation= location.getAltitude();
sample.speed= location.getSpeed();
sample.relativeTime= location.getTime() - workout.start - pauseTime;
sample.absoluteTime= location.getTime();
if(Instance.getInstance(context).pressureAvailable){
sample.tmpPressure= Instance.getInstance(context).lastPressure;
}else{
sample.tmpPressure= -1;
}
synchronized (samples){
samples.add(sample);
}
}
private void initialClearValues(){
lastResume= System.currentTimeMillis();
workout.start= System.currentTimeMillis();
lastPause= 0;
time= 0;
pauseTime= 0;
this.distance= 0;
samples.clear();
}
public int getDistanceInMeters() {
return (int)distance;
}
@ -300,15 +307,16 @@ public class WorkoutRecorder implements LocationListener.LocationChangeListener
SIGNAL_OKAY(Color.GREEN),
SIGNAL_BAD(Color.YELLOW);
public int color;
public final int color;
GpsState(int color) {
this.color = color;
}
}
public interface GpsStateChangedListener{
public interface WorkoutRecorderListener {
void onGPSStateChanged(GpsState oldState, GpsState state);
void onAutoStop();
}
}

View File

@ -0,0 +1,209 @@
/*
* Copyright (c) 2020 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.recording;
import android.content.Context;
import android.hardware.SensorManager;
import android.util.Log;
import java.io.IOException;
import java.util.List;
import de.tadris.fitness.Instance;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.util.AltitudeCorrection;
import de.tadris.fitness.util.CalorieCalculator;
class WorkoutSaver {
private final Context context;
private final Workout workout;
private final List<WorkoutSample> samples;
private final AppDatabase db;
public WorkoutSaver(Context context, Workout workout, List<WorkoutSample> samples) {
this.context = context;
this.workout = workout;
this.samples = samples;
db= Instance.getInstance(context).db;
}
public void saveWorkout(){
setIds();
clearSamplesWithSameTime();
setSimpleValues();
setTopSpeed();
setElevation();
setAscentAndDescent();
setCalories();
storeInDatabase();
}
private void setIds(){
workout.id= System.currentTimeMillis();
int i= 0;
for(WorkoutSample sample : samples) {
i++;
sample.id = workout.id + i;
sample.workoutId = workout.id;
}
}
private void clearSamplesWithSameTime(){
for(int i= samples.size()-2; i >= 0; i--){
WorkoutSample sample= samples.get(i);
WorkoutSample lastSample= samples.get(i+1);
if(sample.absoluteTime == lastSample.absoluteTime){
samples.remove(lastSample);
Log.i("WorkoutManager", "Removed samples at " + sample.absoluteTime + " rel: " + sample.relativeTime + "; " + lastSample.relativeTime);
}
}
}
private void setSimpleValues(){
double length= 0;
for(int i= 1; i < samples.size(); i++){
double sampleLength= samples.get(i - 1).toLatLong().sphericalDistance(samples.get(i).toLatLong());
length+= sampleLength;
}
workout.length= (int)length;
workout.avgSpeed= ((double) workout.length) / ((double) workout.duration / 1000);
workout.avgPace= ((double)workout.duration / 1000 / 60) / ((double) workout.length / 1000);
}
private void setTopSpeed(){
double topSpeed= 0;
for(WorkoutSample sample : samples){
if(sample.speed > topSpeed){
topSpeed= sample.speed;
}
}
workout.topSpeed= topSpeed;
}
private void setElevation() {
setCorrectedElevation();
setPressureElevation();
}
private void setCorrectedElevation() {
// Please see the AltitudeCorrection.java for the reason of this
try {
int lat = (int) Math.round(samples.get(0).lat);
int lon = (int) Math.round(samples.get(0).lon);
AltitudeCorrection correction = new AltitudeCorrection(context, lat, lon);
for (WorkoutSample sample : samples) {
sample.elevation = correction.getHeightOverSeaLevel(sample.elevation);
}
} catch (IOException e) {
// If we can't read the file, we cannot correct the values
e.printStackTrace();
}
}
private void setPressureElevation() {
boolean pressureDataAvailable= samples.get(0).tmpPressure != -1;
if(!pressureDataAvailable){
// Because pressure data isn't available we just use the use GPS elevation
// in WorkoutSample.elevation which was already set
return;
}
double avgElevation= getAverageElevation();
double avgPressure= getAveragePressure();
for(int i= 0; i < samples.size(); i++){
WorkoutSample sample= samples.get(i);
// Altitude Difference to Average Elevation in meters
float altitude_difference =
SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, sample.tmpPressure) -
SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, (float) avgPressure);
sample.elevation= avgElevation + altitude_difference;
}
}
private double getAverageElevation(){
return getAverageElevation(samples);
}
private double getAverageElevation(List<WorkoutSample> samples){
double elevationSum= 0; // Sum of elevation
for(WorkoutSample sample : samples){
elevationSum+= sample.elevation;
}
return elevationSum / samples.size();
}
private double getAveragePressure(){
double pressureSum= 0;
for(WorkoutSample sample : samples){
pressureSum+= sample.tmpPressure;
}
return pressureSum / samples.size();
}
private void setAscentAndDescent(){
workout.ascent = 0;
workout.descent = 0;
// First calculate a floating average to eliminate pressure noise to influence our ascent/descent
int range = 7;
for(int i= 0; i < samples.size(); i++){
int minIndex = Math.max(i - range, 0);
int maxIndex = Math.min(i + range, samples.size() - 1);
samples.get(i).tmpElevation = getAverageElevation(samples.subList(minIndex, maxIndex));
}
// Now sum up the ascent/descent
for(int i= 0; i < samples.size(); i++) {
WorkoutSample sample = samples.get(i);
sample.elevation= sample.tmpElevation;
if(i >= 1){
WorkoutSample lastSample= samples.get(i-1);
double diff= sample.elevation - lastSample.elevation;
if(diff > 0){
// If this sample is higher than the last one, add difference to ascent
workout.ascent += diff;
}else{
// If this sample is lower than the last one, add difference to descent
workout.descent += Math.abs(diff);
}
}
}
}
private void setCalories() {
// Ascent has to be set previously
workout.calorie = CalorieCalculator.calculateCalories(workout, Instance.getInstance(context).userPreferences.getUserWeight());
}
private void storeInDatabase(){
db.workoutDao().insertWorkoutAndSamples(workout, samples.toArray(new WorkoutSample[0]));
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import android.preference.PreferenceManager;
import androidx.annotation.StringRes;
import de.tadris.fitness.recording.WorkoutRecorder;
public abstract class Announcement {
private Context context;
Announcement(Context context) {
this.context = context;
}
public boolean isEnabled() {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("announcement_" + getId(), isEnabledByDefault());
}
protected String getString(@StringRes int resId) {
return context.getString(resId);
}
public abstract String getId();
abstract boolean isEnabledByDefault();
abstract String getSpoken(WorkoutRecorder recorder);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class AnnouncementAverageSpeed extends Announcement {
public AnnouncementAverageSpeed(Context context) {
super(context);
}
@Override
public String getId() {
return "avgSpeed";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
String avgSpeed = UnitUtils.getSpeed(recorder.getAvgSpeed());
return getString(R.string.workoutAvgSpeedLong) + ": " + avgSpeed + ".";
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class AnnouncementDistance extends Announcement {
public AnnouncementDistance(Context context) {
super(context);
}
@Override
public String getId() {
return "distance";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
final String distance = UnitUtils.getDistance(recorder.getDistanceInMeters());
return getString(R.string.workoutDistance) + ": " + distance + ".";
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
public class AnnouncementDuration extends Announcement {
public AnnouncementDuration(Context context) {
super(context);
}
@Override
public String getId() {
return "duration";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
return getString(R.string.workoutDuration) + ": " + getSpokenTime(recorder.getDuration()) + ".";
}
private String getSpokenTime(long duration) {
final long minute = 1000L * 60;
final long hour = minute * 60;
StringBuilder spokenTime = new StringBuilder();
if (duration > hour) {
long hours = duration / hour;
duration = duration % hour; // Set duration to the rest
spokenTime.append(hours).append(" ");
spokenTime.append(getString(hours == 1 ? R.string.timeHourSingular : R.string.timeHourPlural)).append(" ")
.append(getString(R.string.and)).append(" ");
}
long minutes = duration / minute;
spokenTime.append(minutes).append(" ");
spokenTime.append(getString(minutes == 1 ? R.string.timeMinuteSingular : R.string.timeMinutePlural));
return spokenTime.toString();
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
public class AnnouncementGPSStatus extends Announcement {
public AnnouncementGPSStatus(Context context) {
super(context);
}
@Override
public String getId() {
return "gps-lost";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
return "";
}
public String getSpokenGPSLost() {
return getString(R.string.gpsLost);
}
public String getSpokenGPSFound() {
return getString(R.string.gpsFound);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
public class AnnouncementManager {
private Context context;
private List<Announcement> announcements = new ArrayList<>();
public AnnouncementManager(Context context) {
this.context = context;
addAnnouncements();
}
private void addAnnouncements() {
announcements.add(new AnnouncementGPSStatus(context));
announcements.add(new AnnouncementDuration(context));
announcements.add(new AnnouncementDistance(context));
announcements.add(new AnnouncementAverageSpeed(context));
}
public List<Announcement> getAnnouncements() {
return announcements;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -17,17 +17,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.map.tilesource;
package de.tadris.fitness.recording.announcement;
import android.content.Context;
import android.preference.PreferenceManager;
public class TileSources {
public enum AnnouncementMode {
public static FitoTrackTileSource[] tileSources= new FitoTrackTileSource[]{
MapnikTileSource.INSTANCE, HumanitarianTileSource.INSTANCE, ThunderforestTileSource.OUTDOORS, ThunderforestTileSource.CYLE_MAP
};
ALWAYS,
HEADPHONES;
public enum Purpose{
DEFAULT, OUTDOOR, CYCLING
static AnnouncementMode getCurrentMode(Context context) {
String mode = PreferenceManager.getDefaultSharedPreferences(context).getString("announcementMode", "headphones");
assert mode != null;
switch (mode) {
case "always":
return ALWAYS;
default:
case "headphones":
return HEADPHONES;
}
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2020 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.recording.announcement;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothHeadset;
import android.content.Context;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import java.util.Locale;
import de.tadris.fitness.Instance;
import de.tadris.fitness.data.UserPreferences;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class VoiceAnnouncements {
private TextToSpeech textToSpeech;
private boolean ttsAvailable;
private VoiceAnnouncementCallback callback;
private final AnnouncementManager manager;
private long lastSpokenUpdateTime = 0;
private int lastSpokenUpdateDistance = 0;
private final AnnouncementMode currentMode;
private final long intervalTime;
private final int intervalInMeters;
private final AudioManager audioManager;
public VoiceAnnouncements(Context context, VoiceAnnouncementCallback callback) {
this.callback = callback;
UserPreferences prefs = Instance.getInstance(context).userPreferences;
textToSpeech = new TextToSpeech(context, this::ttsReady);
this.intervalTime = 60 * 1000 * prefs.getSpokenUpdateTimePeriod();
this.intervalInMeters = (int) (1000.0 / UnitUtils.CHOSEN_SYSTEM.getDistanceFromKilometers(1) * prefs.getSpokenUpdateDistancePeriod());
this.manager = new AnnouncementManager(context);
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.currentMode = AnnouncementMode.getCurrentMode(context);
}
private void ttsReady(int status) {
ttsAvailable = status == TextToSpeech.SUCCESS && textToSpeech.setLanguage(Locale.getDefault()) >= 0;
if (ttsAvailable) {
textToSpeech.setOnUtteranceProgressListener(new TextToSpeechListener());
}
callback.onVoiceAnnouncementIsReady(ttsAvailable);
}
public void check(WorkoutRecorder recorder) {
if (!ttsAvailable) {
return;
} // Cannot speak
boolean shouldSpeak = false;
if (intervalTime != 0 && recorder.getDuration() - lastSpokenUpdateTime > intervalTime) {
shouldSpeak = true;
}
if (intervalInMeters != 0 && recorder.getDistanceInMeters() - lastSpokenUpdateDistance > intervalInMeters) {
shouldSpeak = true;
}
if (shouldSpeak) {
speak(recorder);
}
}
private void speak(WorkoutRecorder recorder) {
for (Announcement announcement : manager.getAnnouncements()) {
speak(recorder, announcement);
}
lastSpokenUpdateTime = recorder.getDuration();
lastSpokenUpdateDistance = recorder.getDistanceInMeters();
}
private void speak(WorkoutRecorder recorder, Announcement announcement) {
if (!announcement.isEnabled()) {
return;
}
String text = announcement.getSpoken(recorder);
if (!text.equals("")) {
speak(text);
}
}
private int speakId = 1;
public void speak(String text) {
if (!ttsAvailable) {
// Cannot speak
return;
}
if (currentMode == AnnouncementMode.HEADPHONES && !isHeadsetOn()) {
// Not allowed to speak
return;
}
Log.d("Recorder", "TTS speaks: " + text);
textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, "announcement" + (++speakId));
}
private boolean isHeadsetOn() {
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
boolean bluetoothHeadsetConnected = mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()
&& mBluetoothAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED;
return audioManager.isWiredHeadsetOn() || bluetoothHeadsetConnected;
}
public void destroy() {
textToSpeech.shutdown();
}
private class TextToSpeechListener extends UtteranceProgressListener {
@Override
public void onStart(String utteranceId) {
audioManager.requestAudioFocus(null, AudioManager.STREAM_SYSTEM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
}
@Override
public void onDone(String utteranceId) {
audioManager.abandonAudioFocus(null);
}
@Override
public void onError(String utteranceId) {
}
}
public interface VoiceAnnouncementCallback {
void onVoiceAnnouncementIsReady(boolean available);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 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 org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import de.tadris.fitness.R;
/**
* This class has the task to correct the altitude.
* <p>
* In Germany or UK for example the GPS height differs
* roughly 50m from the real elevation over sea level.
* <p>
* The altitude given by GPS is the altitude over the WGS84 reference ellipsoid
* but we want the height over the sea level. That's why we have to correct the height.
* Luckily I found a file containing the corrections for all places around the world.
* <p>
* The geoids.csv is from https://github.com/vectorstofinal/geoid_heights licensed under MIT
*/
public class AltitudeCorrection {
private Context context;
private int latitude, longitude;
private double offset; // Basically how much higher the sea-level than the ellipsoid is
public AltitudeCorrection(Context context, int latitude, int longitude) throws IOException {
this.context = context;
this.latitude = latitude;
this.longitude = longitude;
findOffset();
}
private void findOffset() throws IOException {
InputStream inputStream = context.getResources().openRawResource(R.raw.geoids);
List<String> list = IOUtils.readLines(inputStream, StandardCharsets.UTF_8);
for (String line : list) {
String[] data = line.split(",");
int lat = Integer.parseInt(data[0]);
int lon = Integer.parseInt(data[1]);
double offset = Double.parseDouble(data[2]);
if (lat == this.latitude && lon == this.longitude) {
this.offset = offset;
return;
}
}
}
public double getHeightOverSeaLevel(double heightOverEllipsoid) {
return heightOverEllipsoid - offset;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -20,12 +20,13 @@
package de.tadris.fitness.util;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutType;
public class CalorieCalculator {
/**
*
* workoutType, duration and avgSpeed of workout have to be set
* workoutType, duration, ascent and avgSpeed of workout have to be set
*
* @param workout the workout
* @param weight the weight of the person in kilogram
@ -33,7 +34,8 @@ public class CalorieCalculator {
*/
public static int calculateCalories(Workout workout, double weight){
double mins= (double)(workout.duration / 1000) / 60;
return (int)(mins * (getMET(workout) * 3.5 * weight) / 200);
int ascent= (int)workout.ascent; // 1 calorie per meter
return (int)(mins * (getMET(workout) * 3.5 * weight) / 200) + ascent;
}
/**
@ -42,21 +44,18 @@ public class CalorieCalculator {
* workoutType and avgSpeed of workout have to be set
*
* Calculation currently ignores height.
*
* @param workout
* @return MET
*/
public static double getMET(Workout workout){
private static double getMET(Workout workout) {
double speedInKmh= workout.avgSpeed * 3.6;
if(workout.workoutType.equals(Workout.WORKOUT_TYPE_RUNNING) || workout.workoutType.equals(Workout.WORKOUT_TYPE_HIKING)){
// This is a linear graph based on the website linked above
WorkoutType type = workout.getWorkoutType();
if (type == WorkoutType.RUNNING || type == WorkoutType.HIKING) {
return Math.max(1.5, speedInKmh*1.117 - 2.1906);
}
if(workout.workoutType.equals(Workout.WORKOUT_TYPE_CYCLING)){
// This is a linear graph based on the website linked above
if (type == WorkoutType.CYCLING) {
return Math.max(3, (speedInKmh-10) / 1.5);
}
return -1;
return 0;
}
}

View File

@ -1,3 +1,22 @@
/*
* 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.app.AlertDialog;

View File

@ -0,0 +1,78 @@
package de.tadris.fitness.util;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import org.apache.commons.io.IOUtils;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import de.tadris.fitness.R;
public class FileUtils {
public static void saveOrShareFile(Activity activity, Uri uri, String suffix) {
String[] colors = {activity.getString(R.string.share), activity.getString(R.string.save)};
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setItems(colors, (dialog, which) -> {
if (which == 0) {
shareFile(activity, uri);
} else {
try {
saveFile(activity, uri, suffix);
Toast.makeText(activity, R.string.savedToDownloads, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(activity, R.string.savingFailed, Toast.LENGTH_LONG).show();
}
}
});
builder.show();
}
private static void saveFile(Activity activity, Uri fileUri, String suffix) throws IOException {
File target = new File(Environment.getExternalStorageDirectory(), "Download/fitotrack" + System.currentTimeMillis() + "." + suffix);
if (!target.createNewFile()) {
throw new IOException("Cannot write to file " + target);
}
copyFile(activity, fileUri, Uri.fromFile(target));
}
private static void copyFile(Activity activity, Uri sourceUri, Uri targetUri) throws IOException {
InputStream input = activity.getContentResolver().openInputStream(sourceUri);
if (input == null) {
throw new IOException("Source file not found");
}
OutputStream output = activity.getContentResolver().openOutputStream(targetUri);
IOUtils.copy(input, output);
}
private static void shareFile(Activity activity, Uri uri) {
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setDataAndType(uri, activity.getContentResolver().getType(uri));
intentShareFile.putExtra(Intent.EXTRA_STREAM, uri);
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(Intent.createChooser(intentShareFile, activity.getString(R.string.shareFile)));
Log.d("Export", uri.toString());
Log.d("Export", activity.getContentResolver().getType(uri));
try {
Log.d("Export", new BufferedInputStream(activity.getContentResolver().openInputStream(uri)).toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2020 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 android.preference.PreferenceManager;
import androidx.annotation.StyleRes;
import de.tadris.fitness.R;
import de.tadris.fitness.data.WorkoutType;
public class FitoTrackThemes {
private static final int THEME_SETTING_LIGHT = 0;
private static final int THEME_SETTING_DARK = 1;
private Context context;
public FitoTrackThemes(Context context) {
this.context = context;
}
@StyleRes
public int getDefaultTheme() {
if (shouldUseLightMode()) {
return R.style.AppTheme;
} else {
return R.style.AppThemeDark;
}
}
@StyleRes
public int getWorkoutTypeTheme(WorkoutType type) {
if (shouldUseLightMode()) {
return type.lightTheme;
} else {
return type.darkTheme;
}
}
private boolean shouldUseLightMode() {
switch (getThemeSetting()) {
default:
case THEME_SETTING_LIGHT:
return true;
case THEME_SETTING_DARK:
return false;
}
}
private int getThemeSetting() {
String setting = PreferenceManager.getDefaultSharedPreferences(context).getString("themeSetting", String.valueOf(THEME_SETTING_LIGHT));
assert setting != null;
return Integer.parseInt(setting);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -28,7 +28,7 @@ import de.tadris.fitness.R;
public class NotificationHelper {
public static String CHANNEL_WORKOUT= "workout";
public static final String CHANNEL_WORKOUT = "workout";
public static void createChannels(Context context){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View File

@ -1,46 +0,0 @@
/*
* 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 de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
public class WorkoutTypeCalculator {
public static int getType(Workout workout){
if(workout.workoutType.equals(Workout.WORKOUT_TYPE_RUNNING)){
if(workout.avgSpeed < 1.9){
return R.string.workoutTypeWalking;
}else if(workout.avgSpeed < 2.7){
return R.string.workoutTypeJogging;
}else{
return R.string.workoutTypeRunning;
}
}
if(workout.workoutType.equals(Workout.WORKOUT_TYPE_CYCLING)){
return R.string.workoutTypeCycling;
}
if(workout.workoutType.equals(Workout.WORKOUT_TYPE_HIKING)){
return R.string.workoutTypeHiking;
}
return R.string.workoutTypeUnknown;
}
}

View File

@ -1,132 +0,0 @@
/*
* 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.export;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.preference.PreferenceManager;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import de.tadris.fitness.Instance;
import de.tadris.fitness.R;
import de.tadris.fitness.data.AppDatabase;
import de.tadris.fitness.data.UserPreferences;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.data.WorkoutSample;
import de.tadris.fitness.util.unit.UnitUtils;
public class Exporter {
public static final int VERSION= 1;
public static void exportData(Context context, File output, ExportStatusListener listener) throws IOException {
listener.onStatusChanged(0, context.getString(R.string.initialising));
UserPreferences preferences= Instance.getInstance(context).userPreferences;
AppDatabase database= Instance.getInstance(context).db;
UnitUtils.setUnit(context);
FitoTrackDataContainer container= new FitoTrackDataContainer();
container.version= VERSION;
container.workouts= new ArrayList<>();
container.samples= new ArrayList<>();
listener.onStatusChanged(10, context.getString(R.string.preferences));
FitoTrackSettings settings= new FitoTrackSettings();
settings.weight= preferences.getUserWeight();
settings.mapStyle= preferences.getMapStyle();
settings.preferredUnitSystem= String.valueOf(UnitUtils.CHOSEN_SYSTEM.getId());
container.settings= settings;
listener.onStatusChanged(20, context.getString(R.string.workouts));
container.workouts.addAll(Arrays.asList(database.workoutDao().getWorkouts()));
listener.onStatusChanged(40, context.getString(R.string.locationData));
container.samples.addAll(Arrays.asList(database.workoutDao().getSamples()));
listener.onStatusChanged(60, context.getString(R.string.converting));
XmlMapper mapper= new XmlMapper();
mapper.writeValue(output, container);
listener.onStatusChanged(100, context.getString(R.string.finished));
}
@SuppressLint("ApplySharedPref")
public static void importData(Context context, Uri input, ExportStatusListener listener) throws IOException, UnsupportedVersionException {
listener.onStatusChanged(0, context.getString(R.string.loadingFile));
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.configure(JsonParser.Feature.IGNORE_UNDEFINED, true);
FitoTrackDataContainer container = xmlMapper.readValue(context.getContentResolver().openInputStream(input), FitoTrackDataContainer.class);
if(container.version != 1){
throw new UnsupportedVersionException("Version Code" + container.version + " is unsupported!");
}
listener.onStatusChanged(40, context.getString(R.string.preferences));
PreferenceManager.getDefaultSharedPreferences(context)
.edit().clear()
.putInt("weight", container.settings.weight)
.putString("unitSystem", container.settings.preferredUnitSystem)
.putBoolean("firstStart", false).putString("mapStyle", container.settings.mapStyle)
.commit();
AppDatabase database= Instance.getInstance(context).db;
database.runInTransaction(() -> {
database.clearAllTables();
listener.onStatusChanged(60, context.getString(R.string.workouts));
if(container.workouts != null){
for(Workout workout : container.workouts){
database.workoutDao().insertWorkout(workout);
}
}
listener.onStatusChanged(80, context.getString(R.string.locationData));
if(container.samples != null){
for(WorkoutSample sample : container.samples){
database.workoutDao().insertSample(sample);
}
}
});
listener.onStatusChanged(100, context.getString(R.string.finished));
}
public interface ExportStatusListener{
void onStatusChanged(int progress, String action);
}
public static class UnsupportedVersionException extends Exception{
public UnsupportedVersionException(String message) {
super(message);
}
}
}

View File

@ -1,59 +0,0 @@
/*
* 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.export;
public class FitoTrackSettings {
String preferredUnitSystem;
int weight;
String mapStyle;
public FitoTrackSettings(){}
public FitoTrackSettings(String preferredUnitSystem, int weight, String mapStyle) {
this.preferredUnitSystem = preferredUnitSystem;
this.weight = weight;
this.mapStyle = mapStyle;
}
public String getPreferredUnitSystem() {
return preferredUnitSystem;
}
public void setPreferredUnitSystem(String preferredUnitSystem) {
this.preferredUnitSystem = preferredUnitSystem;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public String getMapStyle() {
return mapStyle;
}
public void setMapStyle(String mapStyle) {
this.mapStyle = mapStyle;
}
}

View File

@ -1,3 +1,22 @@
/*
* Copyright (c) 2020 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.gpx;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
@ -18,7 +37,7 @@ public class Gpx {
Metadata metadata;
String name;
String desc;
private String desc;
@JacksonXmlElementWrapper(useWrapping = false)
List<Track> trk;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -19,6 +19,7 @@
package de.tadris.fitness.util.gpx;
import android.annotation.SuppressLint;
import android.content.Context;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
@ -40,7 +41,7 @@ public class GpxExporter {
mapper.writeValue(file, getGpxFromWorkout(context, workout));
}
public static Gpx getGpxFromWorkout(Context context, Workout workout){
private static Gpx getGpxFromWorkout(Context context, Workout workout) {
Gpx gpx= new Gpx();
gpx.name= workout.toString();
gpx.version= "1.1";
@ -52,7 +53,7 @@ public class GpxExporter {
return gpx;
}
public static Track getTrackFromWorkout(Context context, Workout workout, int number){
private static Track getTrackFromWorkout(Context context, Workout workout, int number) {
WorkoutSample[] samples= Instance.getInstance(context).db.workoutDao().getAllSamplesOfWorkout(workout.id);
Track track= new Track();
track.number= number;
@ -60,7 +61,7 @@ public class GpxExporter {
track.cmt= workout.comment;
track.desc= workout.comment;
track.src= "FitoTrack";
track.type= workout.workoutType;
track.type = workout.getWorkoutType().id;
track.trkseg= new ArrayList<>();
TrackSegment segment= new TrackSegment();
@ -77,13 +78,14 @@ public class GpxExporter {
return track;
}
@SuppressLint("SimpleDateFormat")
private static final SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
public static String getDateTime(long time){
private static String getDateTime(long time) {
return getDateTime(new Date(time));
}
public static String getDateTime(Date date){
private static String getDateTime(Date date) {
return formatter.format(date);
}

View File

@ -1,10 +1,29 @@
/*
* Copyright (c) 2020 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.gpx;
public class Metadata {
String name;
String desc;
String time;
private String name;
private String desc;
private String time;
public Metadata() {
}

View File

@ -1,3 +1,22 @@
/*
* 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.gpx;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;

View File

@ -1,22 +1,43 @@
/*
* Copyright (c) 2020 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.gpx;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
public class TrackPoint {
class TrackPoint {
@JacksonXmlProperty(isAttribute = true)
private
double lat;
@JacksonXmlProperty(isAttribute = true)
private
double lon;
double ele;
private double ele;
String time;
private String time;
String fix;
private String fix;
TrackPointExtension extensions;
private TrackPointExtension extensions;
public TrackPoint(){}

View File

@ -1,8 +1,27 @@
/*
* Copyright (c) 2020 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.gpx;
public class TrackPointExtension {
double speed;
private double speed;
public TrackPointExtension(){}

View File

@ -1,10 +1,29 @@
/*
* Copyright (c) 2020 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.gpx;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import java.util.List;
public class TrackSegment {
class TrackSegment {
@JacksonXmlElementWrapper(useWrapping = false)
List<TrackPoint> trkpt;

View File

@ -1,3 +1,22 @@
/*
* 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.unit;
public class Imperial implements Unit {

View File

@ -1,3 +1,22 @@
/*
* 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.unit;
public class ImperialWithMeters extends Imperial {

View File

@ -1,3 +1,22 @@
/*
* 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.unit;
public class Metric implements Unit{

View File

@ -1,3 +1,22 @@
/*
* 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.unit;
public class MetricPhysical extends Metric{

View File

@ -1,3 +1,22 @@
/*
* 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.unit;
public interface Unit {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -24,22 +24,23 @@ import android.preference.PreferenceManager;
public class UnitUtils {
public static final Unit UNITS_METRIC= new Metric();
public static final Unit UNITS_METRIC_PHYSICAL= new MetricPhysical();
public static final Unit UNITS_IMPERIAL_YARDS= new Imperial();
public static final Unit UNITS_IMPERIAL_METERS= new ImperialWithMeters();
public static final Unit[] supportedUnits= new Unit[] {
private static final Unit UNITS_METRIC = new Metric();
private static final Unit UNITS_METRIC_PHYSICAL = new MetricPhysical();
private static final Unit UNITS_IMPERIAL_YARDS = new Imperial();
private static final Unit UNITS_IMPERIAL_METERS = new ImperialWithMeters();
private static final Unit[] supportedUnits = new Unit[]{
UNITS_METRIC, UNITS_METRIC_PHYSICAL, UNITS_IMPERIAL_YARDS, UNITS_IMPERIAL_METERS
};
public static Unit CHOSEN_SYSTEM= UNITS_METRIC;
public static void setUnit(Context context){
int id= Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString("unitSystem", String.valueOf(UnitUtils.UNITS_METRIC.getId())));
setUnit(id);
String id = PreferenceManager.getDefaultSharedPreferences(context).getString("unitSystem", String.valueOf(UnitUtils.UNITS_METRIC.getId()));
assert id != null;
setUnit(Integer.parseInt(id));
}
public static void setUnit(int id){
private static void setUnit(int id) {
CHOSEN_SYSTEM= UNITS_METRIC;
for(Unit unit : supportedUnits){
if(id == unit.getId()){
@ -62,47 +63,38 @@ public class UnitUtils {
}
public static String getHourMinuteSecondTime(long time){
long totalSeks= time / 1000;
long totalMins= totalSeks / 60;
long totalSecs = time / 1000;
long totalMins = totalSecs / 60;
long hours= totalMins / 60;
long mins= totalMins % 60;
long seks= totalSeks % 60;
long secs = totalSecs % 60;
String minStr= (mins < 10 ? "0" : "") + mins;
String sekStr= (seks < 10 ? "0" : "") + seks;
String sekStr = (secs < 10 ? "0" : "") + secs;
return hours + ":" + minStr + ":" + sekStr;
}
/**
*
* @param pace Pace in min/km
* @return Pace
*/
public static String getPace(double pace){
public static String getPace(double metricPace) {
double one= CHOSEN_SYSTEM.getDistanceFromKilometers(1);
return round(pace / one, 1) + " min/" + CHOSEN_SYSTEM.getLongDistanceUnit();
double secondsTotal = 60 * metricPace / one;
int minutes = (int) secondsTotal / 60;
int seconds = (int) secondsTotal % 60;
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds + " min/" + CHOSEN_SYSTEM.getLongDistanceUnit();
}
/**
*CHOSEN_SYSTEM.getLongDistanceUnit()
* @param consumption consumption in kcal/km
* @return
*/
public static String getRelativeEnergyConsumption(double consumption){
double one= CHOSEN_SYSTEM.getDistanceFromKilometers(1);
return round(consumption / one, 2) + " kcal/" + CHOSEN_SYSTEM.getLongDistanceUnit();
}
/**
*
* @param distance Distance in meters
* @return String in preferred unit
*/
public static String getDistance(int distance){
double units= CHOSEN_SYSTEM.getDistanceFromMeters(distance);
if(units >= 1000){
return round(units / 1000, 1) + " " + CHOSEN_SYSTEM.getLongDistanceUnit();
public static String getDistance(int distanceInMeters) {
if (distanceInMeters >= 1000) {
return round(CHOSEN_SYSTEM.getDistanceFromKilometers((double) distanceInMeters / 1000d), 1) + " " + CHOSEN_SYSTEM.getLongDistanceUnit();
}else{
return (int)units + " " + CHOSEN_SYSTEM.getShortDistanceUnit();
return (int) CHOSEN_SYSTEM.getDistanceFromMeters(distanceInMeters) + " " + CHOSEN_SYSTEM.getShortDistanceUnit();
}
}
@ -115,7 +107,7 @@ public class UnitUtils {
return round(CHOSEN_SYSTEM.getSpeedFromMeterPerSecond(speed), 1) + " " + CHOSEN_SYSTEM.getSpeedUnit();
}
public static double round(double d, int count){
private static double round(double d, int count) {
return (double)Math.round(d * Math.pow(10, count)) / Math.pow(10, count);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -28,8 +28,8 @@ import de.tadris.fitness.R;
public class ProgressDialogController {
private Activity context;
private Dialog dialog;
private final Activity context;
private final Dialog dialog;
private TextView infoView;
private ProgressBar progressBar;
@ -38,7 +38,7 @@ public class ProgressDialogController {
setTitle(title);
}
public ProgressDialogController(Activity context) {
private ProgressDialogController(Activity context) {
this.context = context;
this.dialog= new Dialog(context);
initDialog();
@ -51,7 +51,7 @@ public class ProgressDialogController {
progressBar= dialog.findViewById(R.id.dialogProgressBar);
}
public void setTitle(String title){
private void setTitle(String title) {
dialog.setTitle(title);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
@ -32,7 +32,6 @@ import java.util.Date;
import de.tadris.fitness.R;
import de.tadris.fitness.data.Workout;
import de.tadris.fitness.util.WorkoutTypeCalculator;
import de.tadris.fitness.util.unit.UnitUtils;
public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutViewHolder>{
@ -40,10 +39,14 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
public static class WorkoutViewHolder extends RecyclerView.ViewHolder{
View root;
TextView lengthText, timeText, dateText, typeText, commentText;
final View root;
final TextView lengthText;
final TextView timeText;
final TextView dateText;
final TextView typeText;
final TextView commentText;
public WorkoutViewHolder(@NonNull View itemView) {
WorkoutViewHolder(@NonNull View itemView) {
super(itemView);
this.root= itemView;
lengthText= itemView.findViewById(R.id.workoutLength);
@ -54,8 +57,8 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
}
}
Workout[] workouts;
WorkoutAdapterListener listener;
private final Workout[] workouts;
private final WorkoutAdapterListener listener;
public WorkoutAdapter(Workout[] workouts, WorkoutAdapterListener listener) {
this.workouts = workouts;
@ -74,7 +77,7 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
public void onBindViewHolder(WorkoutViewHolder holder, final int position) {
Workout workout= workouts[position];
holder.dateText.setText(SimpleDateFormat.getDateTimeInstance().format(new Date(workout.start)));
holder.typeText.setText(WorkoutTypeCalculator.getType(workout));
holder.typeText.setText(workout.getWorkoutType().title);
if(workout.comment != null){
if(workout.comment.length() > 33){
holder.commentText.setText(workout.comment.substring(0, 30) + "...");
@ -86,7 +89,7 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
}
holder.lengthText.setText(UnitUtils.getDistance(workout.length));
holder.timeText.setText(UnitUtils.getHourMinuteTime(workout.duration));
holder.root.setOnClickListener(v -> listener.onItemClick(workout));
holder.root.setOnClickListener(v -> listener.onItemClick(position, workout));
holder.root.setOnLongClickListener(v -> {
listener.onItemLongClick(position, workout);
return true;
@ -100,7 +103,7 @@ public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.WorkoutV
}
public interface WorkoutAdapterListener{
void onItemClick(Workout workout);
void onItemClick(int pos, Workout workout);
void onItemLongClick(int pos, Workout workout);
}

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -20,5 +19,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300">
<alpha android:fromAlpha="0" android:toAlpha="1"/>
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
</set>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~

View File

@ -1,10 +1,29 @@
<!--
~ Copyright (c) 2020 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -1,11 +1,30 @@
<!--
~ Copyright (c) 2020 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:alpha="0.8"
android:tint="#FFFFFF"
android:alpha="0.8">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
</vector>

View File

@ -1,5 +1,5 @@
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -18,11 +18,11 @@
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="@android:color/white"/>
android:fillColor="@android:color/white"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
</vector>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<ScrollView 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.EnterWorkoutActivity">
<LinearLayout
android:id="@+id/enterWorkoutRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp" />
</ScrollView>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -19,10 +18,10 @@
-->
<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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".activity.ListWorkoutsActivity">
<androidx.recyclerview.widget.RecyclerView
@ -36,12 +35,12 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="10dp"
app:menu_animationDelayPerItem="50"
app:menu_colorNormal="@color/colorPrimary"
app:menu_colorPressed="@color/colorPrimaryDark"
app:menu_animationDelayPerItem="50"
app:menu_icon="@drawable/fab_add"
app:menu_labels_showAnimation="@anim/fab_slide_in_from_right"
app:menu_labels_hideAnimation="@anim/fab_slide_out_to_right">
app:menu_labels_hideAnimation="@anim/fab_slide_out_to_right"
app:menu_labels_showAnimation="@anim/fab_slide_in_from_right">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/workoutListRecordRunning"
@ -51,7 +50,7 @@
app:fab_colorNormal="@color/colorPrimaryRunning"
app:fab_colorPressed="@color/colorPrimaryDarkRunning"
app:fab_label="@string/workoutTypeRunning"
app:fab_size="normal"/>
app:fab_size="normal" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/workoutListRecordHiking"
@ -71,7 +70,17 @@
app:fab_colorNormal="@color/colorPrimaryBicycling"
app:fab_colorPressed="@color/colorPrimaryDarkBicycling"
app:fab_label="@string/workoutTypeCycling"
app:fab_size="normal"/>
app:fab_size="normal" />
<com.github.clans.fab.FloatingActionButton
android:id="@+id/workoutListEnter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_add_white"
app:fab_colorNormal="@color/colorPrimary"
app:fab_colorPressed="@color/colorPrimaryDark"
app:fab_label="@string/enterWorkout"
app:fab_size="normal" />
</com.github.clans.fab.FloatingActionMenu>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -32,7 +31,7 @@
<LinearLayout
android:id="@+id/recordMapViewrRoot"
android:id="@+id/recordMapViewerRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -56,6 +55,15 @@
android:text="@string/gps"
android:textColor="@android:color/transparent" />
<TextView
android:id="@+id/recordMapAttribution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_margin="5dp"
android:textColor="@color/textColorLight"
android:text="@string/OpenStreetMapAttribution" />
</FrameLayout>
@ -65,8 +73,8 @@
android:layout_height="200dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="0dp"
android:padding="10dp"
android:orientation="vertical">
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/recordTime"
@ -75,7 +83,7 @@
android:fontFamily="sans-serif-black"
android:text="0:44:08"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textColor="?android:textColorPrimary"
android:textSize="30sp"
android:textStyle="bold" />
@ -117,7 +125,7 @@
android:text="2,06 km"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />
@ -137,7 +145,7 @@
android:text="30 kcal"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />
@ -156,7 +164,7 @@
android:id="@+id/recordInfo3Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/workoutAvgSpeed"
android:text="@string/workoutAvgSpeedShort"
android:textAlignment="center"
android:textAllCaps="true"
android:textStyle="bold" />
@ -168,7 +176,7 @@
android:text="7 km/h"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />
@ -188,7 +196,7 @@
android:text="30 kcal"
android:textAlignment="center"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -35,7 +34,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"></LinearLayout>
android:padding="20dp" />
</ScrollView>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -25,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.ShowWorkoutMapActivity" >
tools:context=".activity.ShowWorkoutMapActivity">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline4"
@ -58,6 +57,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline4"></LinearLayout>
app:layout_constraintTop_toTopOf="@+id/guideline4" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:orientation="horizontal">
<NumberPicker
android:id="@+id/durationPickerHours"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<NumberPicker
android:id="@+id/durationPickerMinutes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<NumberPicker
android:id="@+id/durationPickerSeconds"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp" />
</LinearLayout>

View File

@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Ruslan Sokolovski <russok@gmail.com>
~
~ 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/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<NumberPicker
android:id="@+id/spokenUpdatesTimePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_margin="50dp"
android:layout_alignParentLeft="true"/>
<NumberPicker
android:id="@+id/spokenUpdatesDistancePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_margin="50dp"
android:layout_alignParentRight="true"/>
</RelativeLayout>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@id/uploadDescription"
android:text="@string/description" />
<EditText
android:id="@+id/uploadDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:inputType="textShortMessage|textAutoComplete"
android:singleLine="true" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/trackVisibilityPref" />
<Spinner
android:id="@+id/uploadVisibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dropDownWidth="match_parent"
android:entries="@array/osm_track_visibility"
android:spinnerMode="dropdown" />
</TableRow>
</TableLayout>
<CheckBox
android:id="@+id/uploadCutting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/cut" />
</LinearLayout>

View File

@ -1,7 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
android:layout_height="fill_parent">
<NumberPicker
android:id="@+id/weightPicker"

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="-15dp"
android:layout_marginRight="-15dp"
android:gravity="center_vertical"
android:background="?android:selectableItemBackground">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
android:paddingEnd="15dp">
<TextView
android:id="@+id/lineKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:textAppearance="@android:style/TextAppearance.Material.Medium" />
<LinearLayout
android:id="@+id/lineViewRoot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/lineValue"
android:layout_toEndOf="@+id/lineKey"
android:layout_centerVertical="true"
android:gravity="right|center_vertical"
android:orientation="horizontal">
</LinearLayout>
<TextView
android:id="@+id/lineValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textAlignment="textEnd"
android:textAppearance="@android:style/TextAppearance.Material.Medium" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:dividerHorizontal" />
</LinearLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:minHeight="48dip"
android:paddingStart="20dp"
android:paddingEnd="24dp"
android:textAppearance="@android:style/TextAppearance.Material.Medium"
android:textColor="?android:attr/textColorAlertDialogListItem" />

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -42,7 +41,7 @@
android:fontFamily="sans-serif-black"
android:text=""
android:textAllCaps="true"
android:textColor="@color/textColorLight"
android:textColor="?android:textColorSecondary"
android:textStyle="bold" />
<TextView
@ -52,7 +51,7 @@
android:fontFamily="sans-serif-black"
android:text=""
android:textAllCaps="false"
android:textColor="@color/textLighterBlack"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
@ -74,7 +73,7 @@
android:fontFamily="sans-serif-black"
android:text=""
android:textAllCaps="true"
android:textColor="@color/textColorLight"
android:textColor="?android:textColorSecondary"
android:textStyle="bold" />
<TextView
@ -84,7 +83,7 @@
android:fontFamily="sans-serif-black"
android:text=""
android:textAllCaps="false"
android:textColor="@color/textLighterBlack"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
android:textStyle="bold" />

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2019 Jannis Scheibe <jannis@tadris.de>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
@ -21,8 +20,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?android:selectableItemBackground">
android:background="?android:selectableItemBackground"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
@ -37,7 +36,7 @@
android:layout_weight="1"
android:text="Aug 19, 2019 20:15"
android:textAllCaps="true"
android:textColor="@color/textLighterBlack"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
android:textStyle="bold" />
@ -81,7 +80,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="27min"
android:textColor="@color/textLighterBlack"
android:textColor="?android:textColorPrimary"
android:textSize="30sp" />
</LinearLayout>
@ -90,6 +89,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
android:background="@android:color/darker_gray" />
</LinearLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/actionEnterWorkoutAdd"
android:icon="@drawable/ic_add_white"
android:showAsAction="always"
android:title="@string/save" />
</menu>

Some files were not shown because too many files have changed in this diff Show More