From d35c8a74281ac8b27e77c4a6450f8d841a0719f2 Mon Sep 17 00:00:00 2001 From: jannis Date: Sun, 8 Dec 2019 12:00:38 +0100 Subject: [PATCH 1/2] #24 Upload workouts to OpenStreetMap as GPS-Trace --- app/build.gradle | 8 +- .../fitness/activity/ShowWorkoutActivity.java | 63 +++++++++ .../java/de/tadris/fitness/data/Workout.java | 6 +- .../tadris/fitness/osm/GpsTraceLatLong.java | 29 ++++ .../fitness/osm/OAuthAuthentication.java | 132 ++++++++++++++++++ .../tadris/fitness/osm/OAuthUrlProvider.java | 26 ++++ .../tadris/fitness/osm/OsmTraceUploader.java | 116 +++++++++++++++ app/src/main/res/layout/dialog_upload_osm.xml | 64 +++++++++ app/src/main/res/menu/show_workout_menu.xml | 3 + .../main/res/values/osm_track_visibility.xml | 8 ++ app/src/main/res/values/strings.xml | 10 ++ 11 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java create mode 100644 app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java create mode 100644 app/src/main/java/de/tadris/fitness/osm/OAuthUrlProvider.java create mode 100644 app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java create mode 100644 app/src/main/res/layout/dialog_upload_osm.xml create mode 100644 app/src/main/res/values/osm_track_visibility.xml diff --git a/app/build.gradle b/app/build.gradle index 9980cf3..6e874c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,8 +74,14 @@ dependencies { implementation 'stax:stax-api:1.0.1' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8' + implementation ('de.westnordost:osmapi-traces:1.0') + configurations { + compile.exclude group: 'net.sf.kxml', module: 'kxml2' // already included in Android + } + implementation 'oauth.signpost:signpost-commonshttp4:1.2.1.2' + // Android Room - def room_version = "2.2.0-beta01" + def room_version = "2.2.0" annotationProcessor "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-runtime:$room_version" diff --git a/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java b/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java index 0036a13..b9a6c02 100644 --- a/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java +++ b/app/src/main/java/de/tadris/fitness/activity/ShowWorkoutActivity.java @@ -20,6 +20,8 @@ package de.tadris.fitness.activity; import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Typeface; import android.net.Uri; @@ -28,21 +30,31 @@ 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 android.widget.Toast; import androidx.core.content.FileProvider; import java.io.File; 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.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 { @@ -211,7 +223,55 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils. }).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(); + } + + 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 samples = new ArrayList<>(this.samples); + new OsmTraceUploader(this, mHandler, workout, samples, visibility, oAuthConsumer, cut, description).upload(); + } @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -223,6 +283,9 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils. case R.id.actionExportGpx: exportToGpx(); return true; + case R.id.actionUploadOSM: + prepareUpload(); + return true; } return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/de/tadris/fitness/data/Workout.java b/app/src/main/java/de/tadris/fitness/data/Workout.java index 159ac91..0e9f1d2 100644 --- a/app/src/main/java/de/tadris/fitness/data/Workout.java +++ b/app/src/main/java/de/tadris/fitness/data/Workout.java @@ -80,9 +80,13 @@ public class Workout{ if(comment.length() > 2){ return comment; }else{ - return SimpleDateFormat.getDateTimeInstance().format(new Date(start)); + return getDateString(); } } + public String getDateString(){ + return SimpleDateFormat.getDateTimeInstance().format(new Date(start)); + } + } \ No newline at end of file diff --git a/app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java b/app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java new file mode 100644 index 0000000..2a292ea --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java @@ -0,0 +1,29 @@ +package de.tadris.fitness.osm; + +import de.tadris.fitness.data.WorkoutSample; +import de.westnordost.osmapi.map.data.LatLon; + +public class GpsTraceLatLong implements LatLon { + + private final double latitude; + private final double longitude; + + public GpsTraceLatLong(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + public GpsTraceLatLong(WorkoutSample sample) { + this(sample.lat, sample.lon); + } + + @Override + public double getLatitude() { + return latitude; + } + + @Override + public double getLongitude() { + return longitude; + } +} diff --git a/app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java b/app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java new file mode 100644 index 0000000..bde83ff --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java @@ -0,0 +1,132 @@ +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 OAuthConsumer oAuthConsumer= OAuthUrlProvider.getDefaultConsumer(); + private OAuthProvider oAuthProvider= OAuthUrlProvider.getDefaultProvider(); + + private Handler handler; + private Activity activity; + private ProgressDialogController dialogController; + private SharedPreferences preferences; + private 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(); + } + + public 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); + + } + +} diff --git a/app/src/main/java/de/tadris/fitness/osm/OAuthUrlProvider.java b/app/src/main/java/de/tadris/fitness/osm/OAuthUrlProvider.java new file mode 100644 index 0000000..c76d1d2 --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/osm/OAuthUrlProvider.java @@ -0,0 +1,26 @@ +package de.tadris.fitness.osm; + +import oauth.signpost.OAuthConsumer; +import oauth.signpost.OAuthProvider; +import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; +import oauth.signpost.commonshttp.CommonsHttpOAuthProvider; + +public class OAuthUrlProvider { + + static private final String CONSUMER_KEY= "jFL9grFmAo5ZS720YDDRXdSOb7F0IZQf9lnY1PHq"; + static private final String CONSUMER_SECRET= "oH969vYW60fZLco6E09UQl3uFXqjl4siQbOL0q9q"; + + static OAuthConsumer getDefaultConsumer(){ + return new CommonsHttpOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET); + } + + static OAuthProvider getDefaultProvider(){ + return new CommonsHttpOAuthProvider(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"; + + +} diff --git a/app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java b/app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java new file mode 100644 index 0000000..fc02141 --- /dev/null +++ b/app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java @@ -0,0 +1,116 @@ +package de.tadris.fitness.osm; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +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.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 Activity activity; + private Handler handler; + private Workout workout; + private List samples; + private GpsTraceDetails.Visibility visibility; + private OAuthConsumer consumer; + private boolean cut; + private ProgressDialogController dialogController; + private String description; + + public OsmTraceUploader(Activity activity, Handler handler, Workout workout, List 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(() -> { + Toast.makeText(activity, R.string.uploadFailed, 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 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)); + } + +} diff --git a/app/src/main/res/layout/dialog_upload_osm.xml b/app/src/main/res/layout/dialog_upload_osm.xml new file mode 100644 index 0000000..0a9d176 --- /dev/null +++ b/app/src/main/res/layout/dialog_upload_osm.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/show_workout_menu.xml b/app/src/main/res/menu/show_workout_menu.xml index 9124cf6..fe5e260 100644 --- a/app/src/main/res/menu/show_workout_menu.xml +++ b/app/src/main/res/menu/show_workout_menu.xml @@ -23,6 +23,9 @@ + diff --git a/app/src/main/res/values/osm_track_visibility.xml b/app/src/main/res/values/osm_track_visibility.xml new file mode 100644 index 0000000..a3b286d --- /dev/null +++ b/app/src/main/res/values/osm_track_visibility.xml @@ -0,0 +1,8 @@ + + + + Identifiable + Trackable + Private + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 924d17e..bd74019 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,12 @@ Burned Energy Total Energy Energy Consumption + Uploading + Enter Verification Code + Authentication failed. + Upload + Upload Successful + Upload Failed Ascent Descent @@ -115,4 +121,8 @@ Data Map Style Waiting for GPS + Upload to OSM + Cut the first/last 300 Meters + Track Visibility + Description From a186308ba8e1ad75c32ee7d7af7bdc526246f37a Mon Sep 17 00:00:00 2001 From: jannis Date: Sun, 8 Dec 2019 12:43:46 +0100 Subject: [PATCH 2/2] Update copyright notices --- NOTICE.md | 20 ++++++++++++++++++++ app/build.gradle | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/NOTICE.md b/NOTICE.md index 2aa36ff..3300714 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -24,6 +24,7 @@ Current owners and lead developers are: Ludwig Brinckmann, devemux86 + 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. + Copyright 2015 Dmytro Tarianyk Licensed under the Apache License, Version 2.0 (the "License"); @@ -47,6 +49,24 @@ See the License for the specific language governing permissions and limitations under the License. + + + © 2016-2019 Tobias Zwick. This library is released under the terms of the GNU Lesser General Public License (LGPL). + + + + 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. + This copy of Jackson JSON processor streaming parser/generator is licensed under the diff --git a/app/build.gradle b/app/build.gradle index 6e874c8..4388224 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,20 +67,23 @@ dependencies { implementation 'org.mapsforge:mapsforge-map-android:0.11.0' implementation 'com.caverock:androidsvg:1.3' + // Charts 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' + // Upload to OSM implementation ('de.westnordost:osmapi-traces:1.0') configurations { compile.exclude group: 'net.sf.kxml', module: 'kxml2' // already included in Android } implementation 'oauth.signpost:signpost-commonshttp4:1.2.1.2' - // Android Room + // Android Room Database def room_version = "2.2.0" annotationProcessor "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-runtime:$room_version"