From c017f42d48559afbde45aae3bb809e88e77b4588 Mon Sep 17 00:00:00 2001
From: Philipp Neumann <286449+AquaWolf@users.noreply.github.com>
Date: Sat, 31 Aug 2024 21:46:23 +0200
Subject: [PATCH] Added endurain upload service
---
app/AndroidManifest.xml | 2 -
app/res/drawable/service_endurain.xml | 21 ++
app/src/main/org/runnerup/db/DBHelper.java | 2 +
.../runnerup/export/EndurainSynchronizer.java | 291 ++++++++++++++++++
.../main/org/runnerup/export/SyncManager.java | 2 +
5 files changed, 316 insertions(+), 2 deletions(-)
create mode 100644 app/res/drawable/service_endurain.xml
create mode 100644 app/src/main/org/runnerup/export/EndurainSynchronizer.java
diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml
index 3ed04fc71..89a87a524 100644
--- a/app/AndroidManifest.xml
+++ b/app/AndroidManifest.xml
@@ -45,7 +45,6 @@
-
@@ -67,7 +66,6 @@
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="HardcodedDebugMode,UnusedAttribute"
android:dataExtractionRules="@xml/data_extraction_rules">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/org/runnerup/db/DBHelper.java b/app/src/main/org/runnerup/db/DBHelper.java
index 9472483ca..4ace6ae79 100644
--- a/app/src/main/org/runnerup/db/DBHelper.java
+++ b/app/src/main/org/runnerup/db/DBHelper.java
@@ -34,6 +34,7 @@
import org.runnerup.common.util.Constants;
import org.runnerup.db.entities.DBEntity;
import org.runnerup.export.DropboxSynchronizer;
+import org.runnerup.export.EndurainSynchronizer;
import org.runnerup.export.FileSynchronizer;
import org.runnerup.export.RunKeeperSynchronizer;
import org.runnerup.export.RunalyzeSynchronizer;
@@ -466,6 +467,7 @@ private static void insertAccounts(SQLiteDatabase arg0) {
insertAccount(arg0, RunKeeperSynchronizer.NAME, 1);
insertAccount(arg0, RunningAHEADSynchronizer.NAME, 0);
insertAccount(arg0, StravaSynchronizer.NAME, 1);
+ insertAccount(arg0, EndurainSynchronizer.NAME, 1);
insertAccount(arg0, RunnerUpLiveSynchronizer.NAME, 0);
insertAccount(arg0, FileSynchronizer.NAME, 1);
insertAccount(arg0, RunalyzeSynchronizer.NAME, RunalyzeSynchronizer.ENABLED);
diff --git a/app/src/main/org/runnerup/export/EndurainSynchronizer.java b/app/src/main/org/runnerup/export/EndurainSynchronizer.java
new file mode 100644
index 000000000..f98ffab94
--- /dev/null
+++ b/app/src/main/org/runnerup/export/EndurainSynchronizer.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2020 Timo Lüttig
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.runnerup.export;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.runnerup.R;
+import org.runnerup.common.util.Constants;
+import org.runnerup.db.PathSimplifier;
+import org.runnerup.export.format.GPX;
+import org.runnerup.util.FileNameHelper;
+import org.runnerup.workout.FileFormats;
+import org.runnerup.workout.Sport;
+import java.io.StringWriter;
+import okhttp3.FormBody;
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+
+public class EndurainSynchronizer extends DefaultSynchronizer {
+
+ public static final String NAME = "Endurain";
+
+ private static final String TOKEN_URL_PATH = "/token";
+ private static final String UPLOAD_URL_PATH = "/activities/create/upload";
+
+ private long id = 0;
+
+ private PathSimplifier simplifier;
+ private String username;
+ private String password;
+ private String url;
+ private boolean hasCorrectConfig;
+
+ private String access_token = null;
+
+ public EndurainSynchronizer() {
+ super();
+ }
+
+ public EndurainSynchronizer(PathSimplifier simplifier) {
+ this();
+ this.simplifier = simplifier;
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getPublicUrl() {
+ if (url == null || url.isEmpty()) {
+ return "https://your-endurain.local:98";
+ }
+ return url;
+ }
+
+ @Override
+ public int getIconId() {
+ return R.drawable.service_endurain;
+ }
+
+ @Override
+ public void init(ContentValues config) {
+ id = config.getAsLong("_id");
+ String authToken = config.getAsString(Constants.DB.ACCOUNT.AUTH_CONFIG);
+ if (authToken != null) {
+ try {
+ JSONObject tmp = new JSONObject(authToken);
+ //noinspection ConstantConditions
+ username = tmp.optString("username", null);
+ //noinspection ConstantConditions
+ password = tmp.optString("password", null);
+ //noinspection ConstantConditions
+ url = tmp.optString("url", null);
+ hasCorrectConfig = tmp.optBoolean("hasCorrectConfig", false);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public boolean isConfigured() {
+ return username != null && password != null && url != null && hasCorrectConfig;
+ }
+
+ @NonNull
+ @Override
+ public String getAuthConfig() {
+ JSONObject tmp = new JSONObject();
+ try {
+ tmp.put("username", username);
+ tmp.put("password", password);
+ tmp.put("url", url);
+ tmp.put("hasCorrectConfig", hasCorrectConfig);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return tmp.toString();
+ }
+
+ private Status parseAuthData(JSONObject obj) {
+ try {
+ if (obj.has("access_token")) {
+ access_token = obj.getString("access_token");
+ }
+ hasCorrectConfig = true;
+ return Status.OK;
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return Status.ERROR;
+ }
+
+ @Override
+ public void reset() {
+ username = null;
+ password = null;
+ url = null;
+ hasCorrectConfig = false;
+ }
+
+ @NonNull
+ @Override
+ public Status connect() {
+ Status s = Status.NEED_AUTH;
+ s.authMethod = AuthMethod.USER_PASS_URL;
+ if (username == null || password == null || url == null) {
+ return s;
+ }
+
+ if (access_token != null) {
+ return Status.OK;
+ }
+
+ try {
+ OkHttpClient client = getAuthClient();
+ RequestBody formBody = new FormBody.Builder()
+ .add("grant_type", "password")
+ .add("username", username)
+ .add("password", password)
+ .build();
+
+ Request request = new Request.Builder()
+ .url(url + TOKEN_URL_PATH)
+ .post(formBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+
+ if (response.isSuccessful()) {
+ JSONObject obj = new JSONObject(response.body() != null ? response.body().string() : "");
+ response.close();
+ return parseAuthData(obj);
+ }
+
+ response.close();
+ return Status.ERROR;
+ } catch (Exception e) {
+ return Status.ERROR;
+ }
+ }
+
+ private OkHttpClient getAuthClient() {
+ return new OkHttpClient().newBuilder()
+ .addInterceptor(chain -> chain.proceed(chain.request().newBuilder()
+ .addHeader("X-Client-Type", "mobile")
+ .build()))
+ .addInterceptor(chain -> {
+ if (!TextUtils.isEmpty(access_token)) {
+ return chain.proceed(
+ chain.request()
+ .newBuilder()
+ .header("Authorization", "Bearer " + access_token)
+ .build());
+ }
+ return chain.proceed(chain.request());
+ }).build();
+ }
+
+
+ @NonNull
+ @Override
+ public Status upload(SQLiteDatabase db, final long mID) {
+ Status s = connect();
+ if (s != Status.OK) {
+ return s;
+ }
+
+ Sport sport = Sport.RUNNING;
+ long startTime = 0;
+
+ try {
+ String[] columns = {
+ Constants.DB.ACTIVITY.SPORT,
+ Constants.DB.ACTIVITY.START_TIME,
+ };
+ try (Cursor c = db.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + mID,
+ null, null, null, null)) {
+ if (c.moveToFirst()) {
+ sport = Sport.valueOf(c.getInt(0));
+ startTime = c.getLong(1);
+ }
+ }
+
+ String fileBase = FileNameHelper.getExportFileNameWithModel(startTime, sport.TapiriikType());
+
+ GPX gpx = new GPX(db, true, true, simplifier);
+ StringWriter writer = new StringWriter();
+ gpx.export(mID, writer);
+ s = uploadFile(writer, fileBase, FileFormats.GPX.getValue());
+ } catch (Exception e) {
+ Log.e(getName(), "Error uploading, exception: ", e);
+ s = Status.ERROR;
+ s.ex = e;
+ }
+ return s;
+ }
+
+ private Status uploadFile(StringWriter writer, String fileBase, String fileExt) {
+ Status s;
+ try {
+ OkHttpClient client = getAuthClient();
+
+ RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
+ .addFormDataPart("file", (fileBase.replace("/", "") + fileExt),
+ RequestBody.create(MediaType.parse("application/" + fileExt + "+xml"), writer.toString()))
+ .build();
+
+ Request request = new Request.Builder()
+ .url(url + UPLOAD_URL_PATH)
+ .addHeader("Content-Type", "application/json")
+ .method("POST", requestBody)
+ .build();
+
+ int responseCode;
+ Response response = client.newCall(request).execute();
+
+ s = response.isSuccessful() ? Status.OK : Status.ERROR;
+ } catch (Exception e) {
+ s = Status.ERROR;
+ }
+
+ return s;
+ }
+
+ @Override
+ public boolean checkSupport(Feature f) {
+ switch (f) {
+ case UPLOAD:
+ return true;
+ default:
+ return false;
+ }
+ }
+}
+
diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java
index 59c08c602..571f74020 100644
--- a/app/src/main/org/runnerup/export/SyncManager.java
+++ b/app/src/main/org/runnerup/export/SyncManager.java
@@ -195,6 +195,8 @@ public Synchronizer add(ContentValues config) {
synchronizer = new DropboxSynchronizer(mContext, simplifier);
} else if (synchronizerName.contentEquals(WebDavSynchronizer.NAME)) {
synchronizer = new WebDavSynchronizer(simplifier);
+ } else if (synchronizerName.contentEquals(EndurainSynchronizer.NAME)) {
+ synchronizer = new EndurainSynchronizer(simplifier);
} else {
Log.e(getClass().getName(), "synchronizer does not exist: " + synchronizerName);
}