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 com.google.gson.Gson
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
enum class AuthType {
|
enum class AuthType {
|
||||||
NONE,
|
NONE,
|
||||||
@@ -253,4 +254,32 @@ class EndurainApiClient(
|
|||||||
return null
|
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.graphics.createBitmap
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.mikephil.charting.charts.BarLineChartBase
|
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.R
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
|
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DurationXLabelFormatter
|
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.fit.FitViewerActivity
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.ChartDataRepository
|
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.ChartDataRepository
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts
|
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts
|
||||||
@@ -543,6 +547,11 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.activity_action_upload_to_endurain -> {
|
||||||
|
uploadToEndurain()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.activity_action_dev_inspect_file -> {
|
R.id.activity_action_dev_inspect_file -> {
|
||||||
val intent = Intent(requireContext(), FitViewerActivity::class.java).apply {
|
val intent = Intent(requireContext(), FitViewerActivity::class.java).apply {
|
||||||
putExtra(FitViewerActivity.EXTRA_PATH, File(workout.summary.rawDetailsPath).absolutePath)
|
putExtra(FitViewerActivity.EXTRA_PATH, File(workout.summary.rawDetailsPath).absolutePath)
|
||||||
@@ -627,6 +636,16 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
val devToolsSubMenu = devToolsMenu?.subMenu
|
val devToolsSubMenu = devToolsMenu?.subMenu
|
||||||
devToolsMenu?.isVisible = devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems()
|
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() {
|
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) {
|
private fun shareRawSummary(workout: Workout) {
|
||||||
if (workout.summary.rawSummaryData == null) {
|
if (workout.summary.rawSummaryData == null) {
|
||||||
GB.toast(requireContext(), "No raw summary in this activity", Toast.LENGTH_LONG, GB.WARN)
|
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.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.security.SecureRandom
|
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.
|
* Direct HTTP request using OkHttp.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,12 @@
|
|||||||
android:title="@string/activity_detail_share_gps_label"
|
android:title="@string/activity_detail_share_gps_label"
|
||||||
app:showAsAction="never" />
|
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
|
<item
|
||||||
android:id="@+id/activity_action_dev_tools"
|
android:id="@+id/activity_action_dev_tools"
|
||||||
android:icon="@drawable/ic_developer_mode"
|
android:icon="@drawable/ic_developer_mode"
|
||||||
|
|||||||
Reference in New Issue
Block a user