mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Endurain: Support uploading workout GPX file
This commit is contained in:
+29
@@ -21,6 +21,7 @@ import androidx.core.net.toUri
|
||||
import com.google.gson.Gson
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
enum class AuthType {
|
||||
NONE,
|
||||
@@ -253,4 +254,32 @@ class EndurainApiClient(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload activity file (GPX)
|
||||
*/
|
||||
fun uploadActivity(file: File, callback: (Boolean) -> Unit) {
|
||||
Thread {
|
||||
try {
|
||||
val uri = "$baseUrl/api/v1/activities/create/upload".toUri()
|
||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
||||
|
||||
InternetUtils.uploadBinaryFile(
|
||||
uri = uri,
|
||||
file = file,
|
||||
requestHeaders = headers
|
||||
) { success, responseText ->
|
||||
if (success && responseText != null) {
|
||||
callback(true)
|
||||
} else {
|
||||
LOG.error("Activity upload failed")
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Activity upload error", e)
|
||||
callback(false)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
+48
@@ -41,6 +41,7 @@ import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.mikephil.charting.charts.BarLineChartBase
|
||||
@@ -58,6 +59,9 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DurationXLabelFormatter
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainApiClient
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainSetupViewModel
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainTokenManager
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.ChartDataRepository
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts
|
||||
@@ -543,6 +547,11 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_upload_to_endurain -> {
|
||||
uploadToEndurain()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.activity_action_dev_inspect_file -> {
|
||||
val intent = Intent(requireContext(), FitViewerActivity::class.java).apply {
|
||||
putExtra(FitViewerActivity.EXTRA_PATH, File(workout.summary.rawDetailsPath).absolutePath)
|
||||
@@ -627,6 +636,16 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
val devToolsSubMenu = devToolsMenu?.subMenu
|
||||
devToolsMenu?.isVisible = devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems()
|
||||
}
|
||||
|
||||
val endurainVm: EndurainSetupViewModel by viewModels()
|
||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||
if (server != null) {
|
||||
endurainVm.performTokenRefresh(server) {
|
||||
activity?.runOnUiThread {
|
||||
overflowMenu?.findItem(R.id.activity_action_upload_to_endurain)?.isVisible = hasGpx && endurainVm.isLoggedIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun takeSharedScreenshot() {
|
||||
@@ -732,6 +751,35 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadToEndurain() {
|
||||
val workout = currentWorkout ?: return
|
||||
val gpxFile = ActivitySummaryUtils.getGpxFile(workout.summary)
|
||||
|
||||
if (gpxFile == null) {
|
||||
GB.toast("No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val tokenManager = EndurainTokenManager(requireContext())
|
||||
val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||
val apiClient = EndurainApiClient(serverUrl!!, tokenManager)
|
||||
apiClient.uploadActivity(gpxFile) { success ->
|
||||
if (success)
|
||||
GB.toast("Successfully uploaded to Endurain", Toast.LENGTH_SHORT, GB.INFO)
|
||||
else
|
||||
GB.toast("Error while uploading to Endurain", Toast.LENGTH_SHORT, GB.INFO)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
GB.toast(
|
||||
"Unable to upload GPX file to Endurain: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareRawSummary(workout: Workout) {
|
||||
if (workout.summary.rawSummaryData == null) {
|
||||
GB.toast(requireContext(), "No raw summary in this activity", Toast.LENGTH_LONG, GB.WARN)
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.json.JSONObject
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.security.SecureRandom
|
||||
@@ -149,6 +150,83 @@ class InternetUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadBinaryFile(
|
||||
uri: Uri,
|
||||
file: File,
|
||||
requestHeaders: Map<String, String> = emptyMap(),
|
||||
allowInsecure: Boolean = false,
|
||||
onComplete: (success: Boolean, response: String?) -> Unit
|
||||
) {
|
||||
try {
|
||||
if (!file.exists() || !file.canRead()) {
|
||||
LOG.error("File does not exist or cannot be read: ${file.path}")
|
||||
onComplete(false, null)
|
||||
return
|
||||
}
|
||||
|
||||
val fileName = file.name
|
||||
val mimeType = when (fileName.substringAfterLast('.', "").lowercase()) {
|
||||
"gpx" -> "application/gpx+xml"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
|
||||
val boundary = "----GadgetbridgeFormBoundary${System.currentTimeMillis()}"
|
||||
val multipartBody = buildMultipartBody(file, fileName, mimeType, boundary)
|
||||
|
||||
val headers = requestHeaders.toMutableMap()
|
||||
headers["Content-Type"] = "multipart/form-data; boundary=$boundary"
|
||||
|
||||
val response = if (GBApplication.hasDirectInternetAccess()) {
|
||||
directRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = multipartBody,
|
||||
allowInsecure = allowInsecure
|
||||
)
|
||||
} else {
|
||||
InternetHelperSingleton.send(
|
||||
uri,
|
||||
HttpRequest.Method.POST,
|
||||
headers,
|
||||
multipartBody.toByteArray(),
|
||||
allowInsecure
|
||||
)
|
||||
}
|
||||
|
||||
val responseText = response?.data?.bufferedReader()?.use { it.readText() }
|
||||
onComplete(response != null, responseText)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Uploading $uri failed: ", e)
|
||||
onComplete(false, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMultipartBody(
|
||||
file: File,
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
boundary: String
|
||||
): String {
|
||||
val fileBytes = file.readBytes()
|
||||
val output = ByteArrayOutputStream()
|
||||
val writer = output.bufferedWriter()
|
||||
|
||||
writer.write("--$boundary\r\n")
|
||||
writer.write("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"\r\n")
|
||||
writer.write("Content-Type: $mimeType\r\n")
|
||||
writer.write("\r\n")
|
||||
writer.flush()
|
||||
|
||||
output.write(fileBytes)
|
||||
|
||||
writer.write("\r\n")
|
||||
writer.write("--$boundary--\r\n")
|
||||
writer.flush()
|
||||
|
||||
return output.toString("UTF-8")
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct HTTP request using OkHttp.
|
||||
*/
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
android:title="@string/activity_detail_share_gps_label"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/activity_action_upload_to_endurain"
|
||||
android:icon="@drawable/ic_file_upload"
|
||||
android:title="Upload to Endurain"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/activity_action_dev_tools"
|
||||
android:icon="@drawable/ic_developer_mode"
|
||||
|
||||
Reference in New Issue
Block a user