From 67ee95263487dc3f2c2888271f8df8faeefca808 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Wed, 18 Feb 2026 23:12:43 +0100 Subject: [PATCH] Endurain: Update server-side workout type after uploading --- .../activities/endurain/EndurainApiClient.kt | 99 ++++++++++++++++++- .../workouts/WorkoutDetailsFragment.kt | 14 ++- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainApiClient.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainApiClient.kt index 50d8b14002..840db34055 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainApiClient.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainApiClient.kt @@ -19,7 +19,10 @@ package nodomain.freeyourgadget.gadgetbridge.activities.endurain import android.net.Uri import androidx.core.net.toUri import com.google.gson.Gson +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils +import org.json.JSONArray +import org.json.JSONObject import org.slf4j.LoggerFactory import java.io.File @@ -258,7 +261,7 @@ class EndurainApiClient( /** * Upload activity file (GPX) */ - fun uploadActivity(file: File, callback: (Boolean) -> Unit) { + fun uploadActivity(file: File, callback: (Int?) -> Unit) { Thread { try { val uri = "$baseUrl/api/v1/activities/create/upload".toUri() @@ -270,19 +273,55 @@ class EndurainApiClient( requestHeaders = headers ) { success, responseText -> if (success && responseText != null) { - callback(true) + val jsonArray = JSONArray(responseText) + val firstObject = jsonArray.getJSONObject(0) + val id = firstObject.getInt("id") + callback(id) } else { LOG.error("Activity upload failed") - callback(false) + callback(null) } } } catch (e: Exception) { LOG.error("Activity upload error", e) - callback(false) + callback(null) } }.start() } + /** + * Edit uploaded activity + */ + fun editActivity(id: Int, activityKind: ActivityKind, name: String): Boolean { + try { + val uri = "$baseUrl/api/v1/activities/edit".toUri() + val headers = buildHeaders(AuthType.AUTH_TOKEN) + + var activityType = 10 // Generic workout + if (activityLookup.containsKey(activityKind.ordinal)) { + activityType = activityLookup[activityKind.ordinal]!! + } + + val bodyJson = JSONObject().apply { + put("id", id) + put("activity_type", activityType) + put("name", name) + } + + val result = InternetUtils.doStringRequest( + method = "PUT", + uri = uri, + requestHeaders = headers, + body = bodyJson.toString() + ) + LOG.info("editActivity result: {}", result) + return true + } catch (e: Exception) { + LOG.error("Activity edit error", e) + return false + } + } + /** * Fetch server version string */ @@ -302,4 +341,54 @@ class EndurainApiClient( return null } } -} \ No newline at end of file + + /** + * Lookup map to convert ActivityKind to the integer Endurain expects, + * based on https://docs.endurain.com/developer-guide/supported-types/ + */ + val activityLookup = mapOf( + ActivityKind.RUNNING.ordinal to 1, + ActivityKind.TRAIL_RUN.ordinal to 2, + ActivityKind.VIRTUAL_RUN.ordinal to 3, + ActivityKind.CYCLING.ordinal to 4, + ActivityKind.ROAD_BIKE.ordinal to 4, + ActivityKind.GRAVEL_BIKE.ordinal to 5, + ActivityKind.MOUNTAIN_BIKE.ordinal to 6, + ActivityKind.POOL_SWIM.ordinal to 8, + ActivityKind.SWIMMING_OPENWATER.ordinal to 9, + ActivityKind.TRAINING.ordinal to 10, + ActivityKind.WALKING.ordinal to 11, + ActivityKind.HIKING.ordinal to 12, + ActivityKind.ROWING.ordinal to 13, + ActivityKind.YOGA.ordinal to 14, + ActivityKind.SKIING.ordinal to 15, + ActivityKind.SNOWBOARDING.ordinal to 17, + ActivityKind.TRANSITION.ordinal to 18, + ActivityKind.STRENGTH_TRAINING.ordinal to 19, + ActivityKind.CROSSFIT.ordinal to 20, + ActivityKind.TENNIS.ordinal to 21, + ActivityKind.TABLE_TENNIS.ordinal to 22, + ActivityKind.BADMINTON.ordinal to 23, + ActivityKind.SQUASH.ordinal to 24, + ActivityKind.RACQUETBALL.ordinal to 25, + ActivityKind.PICKLEBALL.ordinal to 26, + ActivityKind.BIKE_COMMUTE.ordinal to 27, + ActivityKind.INDOOR_CYCLING.ordinal to 28, + ActivityKind.WINDSURFING.ordinal to 30, + ActivityKind.INDOOR_WALKING.ordinal to 31, + ActivityKind.STAND_UP_PADDLEBOARDING.ordinal to 32, + ActivityKind.SURFING.ordinal to 33, + ActivityKind.TRACK_RUN.ordinal to 34, + ActivityKind.E_BIKE.ordinal to 35, + ActivityKind.E_MOUNTAIN_BIKE.ordinal to 36, + ActivityKind.ICE_SKATING.ordinal to 37, + ActivityKind.SOCCER.ordinal to 38, + ActivityKind.PADEL.ordinal to 39, + ActivityKind.TREADMILL.ordinal to 40, + ActivityKind.CARDIO.ordinal to 41, + ActivityKind.KAYAKING.ordinal to 42, + ActivityKind.SAILING.ordinal to 43, + ActivityKind.INLINE_SKATING.ordinal to 45, + ActivityKind.HIIT.ordinal to 46 + ) +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt index 32ebc69ed8..90b3a48f72 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt @@ -746,7 +746,10 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider { private fun uploadToEndurain() { val workout = currentWorkout ?: return - val gpxFile = ActivitySummaryUtils.getGpxFile(workout.summary) + val workoutName = currentWorkout!!.summary.name + val activityKind = ActivityKind.fromCode(currentWorkout!!.summary.activityKind) + val activityTrackProvider = gbDevice.deviceCoordinator.getActivityTrackProvider(gbDevice, requireContext()) + val gpxFile = ActivitySummaryUtils.getShareableGpxFile(activityTrackProvider, workout.summary) if (gpxFile == null) { GB.toast("No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO) @@ -758,9 +761,14 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider { val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null) val apiClient = EndurainApiClient(serverUrl!!, endurainVm.tokenManager) endurainVm.tokenManager.performTokenRefresh(serverUrl) { - apiClient.uploadActivity(gpxFile) { success -> + LOG.info("Uploading workout '{}' (type {}) to Endurain", workoutName, activityKind) + apiClient.uploadActivity(gpxFile) { newId -> + if (newId != null) { + // Update activity type on the server + apiClient.editActivity(newId, activityKind, workoutName) + } activity?.runOnUiThread { - if (success) + if (newId != null) GB.toast( "Successfully uploaded to Endurain", Toast.LENGTH_SHORT,