Merge branch 'feature-upload-osm' into develop

This commit is contained in:
jannis 2019-12-08 12:44:02 +01:00
commit 05d43de1a7
12 changed files with 487 additions and 3 deletions

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,24 @@
See the License for the specific language governing permissions and
limitations under the License.
<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

@ -67,15 +67,24 @@ 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'
// Android Room
def room_version = "2.2.0-beta01"
// 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 Database
def room_version = "2.2.0"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-runtime:$room_version"

View File

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

View File

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

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
android:id="@+id/actionExportGpx"
android:title="@string/exportAsGpxFile" />
<item
android:id="@+id/actionUploadOSM"
android:title="@string/actionUploadToOSM" />
<item
android:id="@+id/actionDeleteWorkout"
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="workoutTotalEnergy">Total Energy</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="workoutDescent">Descent</string>
@ -115,4 +121,8 @@
<string name="data">Data</string>
<string name="mapStyle">Map Style</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>