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