#24 Upload workouts to OpenStreetMap as GPS-Trace

This commit is contained in:
jannis 2019-12-08 12:00:38 +01:00
parent 7a61b94696
commit d35c8a7428
11 changed files with 463 additions and 2 deletions

View File

@ -74,8 +74,14 @@ dependencies {
implementation 'stax:stax-api:1.0.1' implementation 'stax:stax-api:1.0.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8' 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 // Android Room
def room_version = "2.2.0-beta01" def room_version = "2.2.0"
annotationProcessor "androidx.room:room-compiler:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"

View File

@ -20,6 +20,8 @@
package de.tadris.fitness.activity; package de.tadris.fitness.activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
@ -28,21 +30,31 @@ import android.util.TypedValue;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import de.tadris.fitness.Instance; import de.tadris.fitness.Instance;
import de.tadris.fitness.R; 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.DialogUtils;
import de.tadris.fitness.util.gpx.GpxExporter; import de.tadris.fitness.util.gpx.GpxExporter;
import de.tadris.fitness.util.unit.UnitUtils; import de.tadris.fitness.util.unit.UnitUtils;
import de.tadris.fitness.view.ProgressDialogController; import de.tadris.fitness.view.ProgressDialogController;
import de.westnordost.osmapi.traces.GpsTraceDetails;
import oauth.signpost.OAuthConsumer;
public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.WorkoutDeleter { public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.WorkoutDeleter {
@ -211,7 +223,55 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
}).start(); }).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<WorkoutSample> samples = new ArrayList<>(this.samples);
new OsmTraceUploader(this, mHandler, workout, samples, visibility, oAuthConsumer, cut, description).upload();
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
@ -223,6 +283,9 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
case R.id.actionExportGpx: case R.id.actionExportGpx:
exportToGpx(); exportToGpx();
return true; return true;
case R.id.actionUploadOSM:
prepareUpload();
return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }

View File

@ -80,9 +80,13 @@ public class Workout{
if(comment.length() > 2){ if(comment.length() > 2){
return comment; return comment;
}else{ }else{
return SimpleDateFormat.getDateTimeInstance().format(new Date(start)); return getDateString();
} }
} }
public String getDateString(){
return SimpleDateFormat.getDateTimeInstance().format(new Date(start));
}
} }

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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<WorkoutSample> 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<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(() -> {
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<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

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<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: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>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent" />
</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

@ -23,6 +23,9 @@
<item <item
android:id="@+id/actionExportGpx" android:id="@+id/actionExportGpx"
android:title="@string/exportAsGpxFile" /> android:title="@string/exportAsGpxFile" />
<item
android:id="@+id/actionUploadOSM"
android:title="@string/actionUploadToOSM" />
<item <item
android:id="@+id/actionDeleteWorkout" android:id="@+id/actionDeleteWorkout"
android:title="@string/delete" /> android:title="@string/delete" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="osm_track_visibility">
<item>Identifiable</item>
<item>Trackable</item>
<item>Private</item>
</string-array>
</resources>

View File

@ -62,6 +62,12 @@
<string name="workoutBurnedEnergy">Burned Energy</string> <string name="workoutBurnedEnergy">Burned Energy</string>
<string name="workoutTotalEnergy">Total Energy</string> <string name="workoutTotalEnergy">Total Energy</string>
<string name="workoutEnergyConsumption">Energy Consumption</string> <string name="workoutEnergyConsumption">Energy Consumption</string>
<string name="uploading">Uploading</string>
<string name="enterVerificationCode">Enter Verification Code</string>
<string name="authenticationFailed">Authentication failed.</string>
<string name="upload">Upload</string>
<string name="uploadSuccessful">Upload Successful</string>
<string name="uploadFailed">Upload Failed</string>
<string name="workoutAscent">Ascent</string> <string name="workoutAscent">Ascent</string>
<string name="workoutDescent">Descent</string> <string name="workoutDescent">Descent</string>
@ -115,4 +121,8 @@
<string name="data">Data</string> <string name="data">Data</string>
<string name="mapStyle">Map Style</string> <string name="mapStyle">Map Style</string>
<string name="waiting_gps">Waiting for GPS</string> <string name="waiting_gps">Waiting for GPS</string>
<string name="actionUploadToOSM">Upload to OSM</string>
<string name="cut">Cut the first/last 300 Meters</string>
<string name="trackVisibilityPref">Track Visibility</string>
<string name="description">Description</string>
</resources> </resources>