mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Wanderer: Add basic integration and support GPX uploading
This commit is contained in:
+32
-31
@@ -26,13 +26,13 @@ import org.json.JSONObject
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
enum class AuthType {
|
enum class EndurainAuthType {
|
||||||
NONE,
|
NONE,
|
||||||
AUTH_TOKEN,
|
AUTH_TOKEN,
|
||||||
REFRESH_TOKEN
|
REFRESH_TOKEN
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LoginResponse(
|
data class EndurainLoginResponse(
|
||||||
val session_id: String? = null,
|
val session_id: String? = null,
|
||||||
val access_token: String? = null,
|
val access_token: String? = null,
|
||||||
val refresh_token: String? = null,
|
val refresh_token: String? = null,
|
||||||
@@ -44,16 +44,16 @@ data class LoginResponse(
|
|||||||
val detail: String? = null
|
val detail: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MfaVerifyRequest(
|
data class EndurainMfaVerifyRequest(
|
||||||
val username: String,
|
val username: String,
|
||||||
val mfa_code: String
|
val mfa_code: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TokenExchangeRequest(
|
data class EndurainTokenExchangeRequest(
|
||||||
val code_verifier: String
|
val code_verifier: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class IdentityProvider(
|
data class EndurainIdentityProvider(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val slug: String
|
val slug: String
|
||||||
@@ -69,16 +69,16 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* Build headers with authentication tokens
|
* Build headers with authentication tokens
|
||||||
*/
|
*/
|
||||||
private fun buildHeaders(auth: AuthType): MutableMap<String, String> {
|
private fun buildHeaders(auth: EndurainAuthType): MutableMap<String, String> {
|
||||||
val headers = mutableMapOf("X-Client-Type" to "mobile")
|
val headers = mutableMapOf("X-Client-Type" to "mobile")
|
||||||
|
|
||||||
when (auth) {
|
when (auth) {
|
||||||
AuthType.AUTH_TOKEN -> {
|
EndurainAuthType.AUTH_TOKEN -> {
|
||||||
tokenManager.getAccessToken()?.let { token ->
|
tokenManager.getAccessToken()?.let { token ->
|
||||||
headers["Authorization"] = "Bearer $token"
|
headers["Authorization"] = "Bearer $token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AuthType.REFRESH_TOKEN -> {
|
EndurainAuthType.REFRESH_TOKEN -> {
|
||||||
tokenManager.getRefreshToken()?.let { token ->
|
tokenManager.getRefreshToken()?.let { token ->
|
||||||
headers["Authorization"] = "Bearer $token"
|
headers["Authorization"] = "Bearer $token"
|
||||||
}
|
}
|
||||||
@@ -92,14 +92,14 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* Username/Password Login
|
* Username/Password Login
|
||||||
*/
|
*/
|
||||||
fun login(username: String, password: String): LoginResponse? {
|
fun login(username: String, password: String): EndurainLoginResponse? {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/auth/login".toUri()
|
val uri = "$baseUrl/api/v1/auth/login".toUri()
|
||||||
|
|
||||||
// Form-encoded body
|
// Form-encoded body
|
||||||
val body = "username=${Uri.encode(username)}&password=${Uri.encode(password)}"
|
val body = "username=${Uri.encode(username)}&password=${Uri.encode(password)}"
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.NONE)
|
val headers = buildHeaders(EndurainAuthType.NONE)
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
val responseText = InternetUtils.doStringRequest(
|
val responseText = InternetUtils.doStringRequest(
|
||||||
@@ -110,7 +110,7 @@ class EndurainApiClient(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return if (responseText != null) {
|
return if (responseText != null) {
|
||||||
gson.fromJson(responseText, LoginResponse::class.java)
|
gson.fromJson(responseText, EndurainLoginResponse::class.java)
|
||||||
} else {
|
} else {
|
||||||
LOG.error("Login failed: empty response")
|
LOG.error("Login failed: empty response")
|
||||||
null
|
null
|
||||||
@@ -124,14 +124,14 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* MFA Verification
|
* MFA Verification
|
||||||
*/
|
*/
|
||||||
fun verifyMfa(username: String, mfaCode: String): LoginResponse? {
|
fun verifyMfa(username: String, mfaCode: String): EndurainLoginResponse? {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/auth/mfa/verify".toUri()
|
val uri = "$baseUrl/api/v1/auth/mfa/verify".toUri()
|
||||||
|
|
||||||
val request = MfaVerifyRequest(username, mfaCode)
|
val request = EndurainMfaVerifyRequest(username, mfaCode)
|
||||||
val body = gson.toJson(request)
|
val body = gson.toJson(request)
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.NONE)
|
val headers = buildHeaders(EndurainAuthType.NONE)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
val responseText = InternetUtils.doStringRequest(
|
val responseText = InternetUtils.doStringRequest(
|
||||||
@@ -142,7 +142,7 @@ class EndurainApiClient(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return if (responseText != null) {
|
return if (responseText != null) {
|
||||||
gson.fromJson(responseText, LoginResponse::class.java)
|
gson.fromJson(responseText, EndurainLoginResponse::class.java)
|
||||||
} else {
|
} else {
|
||||||
LOG.error("MFA verification failed: empty response")
|
LOG.error("MFA verification failed: empty response")
|
||||||
null
|
null
|
||||||
@@ -156,11 +156,11 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* Token Refresh
|
* Token Refresh
|
||||||
*/
|
*/
|
||||||
fun refreshToken(): LoginResponse? {
|
fun refreshToken(): EndurainLoginResponse? {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/auth/refresh".toUri()
|
val uri = "$baseUrl/api/v1/auth/refresh".toUri()
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.REFRESH_TOKEN)
|
val headers = buildHeaders(EndurainAuthType.REFRESH_TOKEN)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
val responseText = InternetUtils.doStringRequest(
|
val responseText = InternetUtils.doStringRequest(
|
||||||
@@ -171,7 +171,7 @@ class EndurainApiClient(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return if (responseText != null) {
|
return if (responseText != null) {
|
||||||
gson.fromJson(responseText, LoginResponse::class.java)
|
gson.fromJson(responseText, EndurainLoginResponse::class.java)
|
||||||
} else {
|
} else {
|
||||||
LOG.error("Token refresh failed: empty response")
|
LOG.error("Token refresh failed: empty response")
|
||||||
null
|
null
|
||||||
@@ -189,7 +189,7 @@ class EndurainApiClient(
|
|||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/auth/logout".toUri()
|
val uri = "$baseUrl/api/v1/auth/logout".toUri()
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
val headers = buildHeaders(EndurainAuthType.AUTH_TOKEN)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
InternetUtils.doStringRequest(
|
InternetUtils.doStringRequest(
|
||||||
@@ -210,11 +210,11 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* Get list of available identity providers
|
* Get list of available identity providers
|
||||||
*/
|
*/
|
||||||
fun getIdentityProviders(): List<IdentityProvider>? {
|
fun getIdentityProviders(): List<EndurainIdentityProvider>? {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/public/idp".toUri()
|
val uri = "$baseUrl/api/v1/public/idp".toUri()
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.NONE)
|
val headers = buildHeaders(EndurainAuthType.NONE)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
val responseText = InternetUtils.doStringRequest(
|
val responseText = InternetUtils.doStringRequest(
|
||||||
@@ -223,7 +223,7 @@ class EndurainApiClient(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return if (responseText != null) {
|
return if (responseText != null) {
|
||||||
val type = object : com.google.gson.reflect.TypeToken<List<IdentityProvider>>() {}.type
|
val type = object : com.google.gson.reflect.TypeToken<List<EndurainIdentityProvider>>() {}.type
|
||||||
gson.fromJson(responseText, type)
|
gson.fromJson(responseText, type)
|
||||||
} else {
|
} else {
|
||||||
LOG.error("Failed to fetch identity providers")
|
LOG.error("Failed to fetch identity providers")
|
||||||
@@ -238,14 +238,14 @@ class EndurainApiClient(
|
|||||||
/**
|
/**
|
||||||
* Exchange OAuth session for tokens (PKCE flow)
|
* Exchange OAuth session for tokens (PKCE flow)
|
||||||
*/
|
*/
|
||||||
fun exchangeOAuthSession(sessionId: String, codeVerifier: String): LoginResponse? {
|
fun exchangeOAuthSession(sessionId: String, codeVerifier: String): EndurainLoginResponse? {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/public/idp/session/$sessionId/tokens".toUri()
|
val uri = "$baseUrl/api/v1/public/idp/session/$sessionId/tokens".toUri()
|
||||||
|
|
||||||
val request = TokenExchangeRequest(codeVerifier)
|
val request = EndurainTokenExchangeRequest(codeVerifier)
|
||||||
val body = gson.toJson(request)
|
val body = gson.toJson(request)
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.NONE)
|
val headers = buildHeaders(EndurainAuthType.NONE)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
val responseText = InternetUtils.doStringRequest(
|
val responseText = InternetUtils.doStringRequest(
|
||||||
@@ -258,7 +258,7 @@ class EndurainApiClient(
|
|||||||
LOG.debug("OAuth token result: $responseText")
|
LOG.debug("OAuth token result: $responseText")
|
||||||
|
|
||||||
return if (responseText != null) {
|
return if (responseText != null) {
|
||||||
gson.fromJson(responseText, LoginResponse::class.java)
|
gson.fromJson(responseText, EndurainLoginResponse::class.java)
|
||||||
} else {
|
} else {
|
||||||
LOG.error("OAuth token exchange failed: empty response")
|
LOG.error("OAuth token exchange failed: empty response")
|
||||||
null
|
null
|
||||||
@@ -280,7 +280,7 @@ class EndurainApiClient(
|
|||||||
try {
|
try {
|
||||||
val uri = "$baseUrl$endpoint".toUri()
|
val uri = "$baseUrl$endpoint".toUri()
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
val headers = buildHeaders(EndurainAuthType.AUTH_TOKEN)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
return InternetUtils.doStringRequest(
|
return InternetUtils.doStringRequest(
|
||||||
@@ -302,14 +302,15 @@ class EndurainApiClient(
|
|||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/activities/create/upload".toUri()
|
val uri = "$baseUrl/api/v1/activities/create/upload".toUri()
|
||||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
val headers = buildHeaders(EndurainAuthType.AUTH_TOKEN)
|
||||||
|
|
||||||
InternetUtils.uploadBinaryFile(
|
InternetUtils.uploadBinaryFile(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
file = file,
|
file = file,
|
||||||
requestHeaders = headers
|
requestHeaders = headers
|
||||||
) { success, responseText ->
|
) { success, statusCode, responseText ->
|
||||||
if (success && responseText != null) {
|
if (success && responseText != null) {
|
||||||
|
LOG.debug("Response $statusCode from Endurain: $responseText")
|
||||||
val jsonArray = JSONArray(responseText)
|
val jsonArray = JSONArray(responseText)
|
||||||
val firstObject = jsonArray.getJSONObject(0)
|
val firstObject = jsonArray.getJSONObject(0)
|
||||||
val id = firstObject.getInt("id")
|
val id = firstObject.getInt("id")
|
||||||
@@ -332,7 +333,7 @@ class EndurainApiClient(
|
|||||||
fun editActivity(id: Int, activityKind: ActivityKind, name: String): Boolean {
|
fun editActivity(id: Int, activityKind: ActivityKind, name: String): Boolean {
|
||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/activities/edit".toUri()
|
val uri = "$baseUrl/api/v1/activities/edit".toUri()
|
||||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
val headers = buildHeaders(EndurainAuthType.AUTH_TOKEN)
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
var activityType = 10 // Generic workout
|
var activityType = 10 // Generic workout
|
||||||
@@ -367,7 +368,7 @@ class EndurainApiClient(
|
|||||||
try {
|
try {
|
||||||
val uri = "$baseUrl/api/v1/about".toUri()
|
val uri = "$baseUrl/api/v1/about".toUri()
|
||||||
|
|
||||||
val headers = buildHeaders(AuthType.NONE)
|
val headers = buildHeaders(EndurainAuthType.NONE)
|
||||||
|
|
||||||
val result = InternetUtils.doJsonRequest(
|
val result = InternetUtils.doJsonRequest(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() {
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSsoLogin(provider: IdentityProvider) {
|
private fun launchSsoLogin(provider: EndurainIdentityProvider) {
|
||||||
val ssoUrl = vm.generateSsoUrl(provider.slug)
|
val ssoUrl = vm.generateSsoUrl(provider.slug)
|
||||||
|
|
||||||
LOG.info("Launching secure browser for SSO URL: $ssoUrl")
|
LOG.info("Launching secure browser for SSO URL: $ssoUrl")
|
||||||
|
|||||||
+13
-13
@@ -40,14 +40,14 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
SSO_PROVIDERS
|
SSO_PROVIDERS
|
||||||
}
|
}
|
||||||
|
|
||||||
val tokenManager = EndurainTokenManager(application)
|
val endurainTokenManager = EndurainTokenManager(application)
|
||||||
var step = Step.SERVER
|
var step = Step.SERVER
|
||||||
var server = ""
|
var server = ""
|
||||||
var localLoginEnabled = false
|
var localLoginEnabled = false
|
||||||
var ssoEnabled = false
|
var ssoEnabled = false
|
||||||
var pendingMfaUsername: String? = null
|
var pendingMfaUsername: String? = null
|
||||||
var availableProviders: List<IdentityProvider> = emptyList()
|
var availableProviders: List<EndurainIdentityProvider> = emptyList()
|
||||||
var serverVersion: String? = null
|
var endurainServerVersion: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch server version
|
* Fetch server version
|
||||||
@@ -58,8 +58,8 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
) {
|
) {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
apiClient = EndurainApiClient(serverUrl, tokenManager)
|
apiClient = EndurainApiClient(serverUrl, endurainTokenManager)
|
||||||
serverVersion = apiClient.fetchVersion()
|
endurainServerVersion = apiClient.fetchVersion()
|
||||||
callback(true)
|
callback(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LOG.error("Fetching server version error", e)
|
LOG.error("Fetching server version error", e)
|
||||||
@@ -119,7 +119,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
fun fetchSsoProviders(callback: (Boolean) -> Unit) {
|
fun fetchSsoProviders(callback: (Boolean) -> Unit) {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
apiClient = EndurainApiClient(server, tokenManager)
|
apiClient = EndurainApiClient(server, endurainTokenManager)
|
||||||
val providers = apiClient.getIdentityProviders()
|
val providers = apiClient.getIdentityProviders()
|
||||||
|
|
||||||
if (providers != null && providers.isNotEmpty()) {
|
if (providers != null && providers.isNotEmpty()) {
|
||||||
@@ -183,14 +183,14 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
prefs.edit { remove("code_verifier") }
|
prefs.edit { remove("code_verifier") }
|
||||||
|
|
||||||
if (!::apiClient.isInitialized) {
|
if (!::apiClient.isInitialized) {
|
||||||
apiClient = EndurainApiClient(server, tokenManager)
|
apiClient = EndurainApiClient(server, endurainTokenManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = apiClient.exchangeOAuthSession(sessionId, verifier)
|
val response = apiClient.exchangeOAuthSession(sessionId, verifier)
|
||||||
|
|
||||||
if (response?.access_token != null) {
|
if (response?.access_token != null) {
|
||||||
LOG.info("SSO login successful")
|
LOG.info("SSO login successful")
|
||||||
tokenManager.saveTokens(
|
endurainTokenManager.saveTokens(
|
||||||
response.access_token,
|
response.access_token,
|
||||||
response.refresh_token!!,
|
response.refresh_token!!,
|
||||||
response.expires_in!!,
|
response.expires_in!!,
|
||||||
@@ -219,7 +219,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
) {
|
) {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
apiClient = EndurainApiClient(serverUrl, tokenManager)
|
apiClient = EndurainApiClient(serverUrl, endurainTokenManager)
|
||||||
val response = apiClient.login(username, password)
|
val response = apiClient.login(username, password)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -235,7 +235,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
}
|
}
|
||||||
response.access_token != null -> {
|
response.access_token != null -> {
|
||||||
LOG.info("Login successful")
|
LOG.info("Login successful")
|
||||||
tokenManager.saveTokens(
|
endurainTokenManager.saveTokens(
|
||||||
response.access_token,
|
response.access_token,
|
||||||
response.refresh_token!!,
|
response.refresh_token!!,
|
||||||
response.expires_in!!,
|
response.expires_in!!,
|
||||||
@@ -274,7 +274,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
|
|
||||||
if (response?.access_token != null) {
|
if (response?.access_token != null) {
|
||||||
LOG.info("MFA verification successful")
|
LOG.info("MFA verification successful")
|
||||||
tokenManager.saveTokens(
|
endurainTokenManager.saveTokens(
|
||||||
response.access_token,
|
response.access_token,
|
||||||
response.refresh_token!!,
|
response.refresh_token!!,
|
||||||
response.expires_in!!,
|
response.expires_in!!,
|
||||||
@@ -302,12 +302,12 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
if (::apiClient.isInitialized) {
|
if (::apiClient.isInitialized) {
|
||||||
apiClient.logout()
|
apiClient.logout()
|
||||||
} else {
|
} else {
|
||||||
tokenManager.clearTokens()
|
endurainTokenManager.clearTokens()
|
||||||
}
|
}
|
||||||
callback(true)
|
callback(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LOG.error("Logout error", e)
|
LOG.error("Logout error", e)
|
||||||
tokenManager.clearTokens() // Clear tokens anyway
|
endurainTokenManager.clearTokens() // Clear tokens anyway
|
||||||
callback(true)
|
callback(true)
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ import nodomain.freeyourgadget.gadgetbridge.R
|
|||||||
|
|
||||||
class EndurainSsoProviderDialog(
|
class EndurainSsoProviderDialog(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val providers: List<IdentityProvider>,
|
private val providers: List<EndurainIdentityProvider>,
|
||||||
private val onProviderSelected: (IdentityProvider) -> Unit
|
private val onProviderSelected: (EndurainIdentityProvider) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
|
|||||||
+57
-22
@@ -40,8 +40,8 @@ class OnlineFitnessTrackersPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
setPreferencesFromResource(R.xml.online_fitness_trackers_preferences, rootKey)
|
setPreferencesFromResource(R.xml.online_fitness_trackers_preferences, rootKey)
|
||||||
|
|
||||||
updateNetworkWarning()
|
updateNetworkWarning()
|
||||||
wireLoginPreference()
|
wireLoginPreferences()
|
||||||
wireLogoutPreference()
|
wireLogoutPreferences()
|
||||||
updateStatus()
|
updateStatus()
|
||||||
updateLogoutPreferenceVisibility()
|
updateLogoutPreferenceVisibility()
|
||||||
setupLoginResultListener()
|
setupLoginResultListener()
|
||||||
@@ -50,8 +50,8 @@ class OnlineFitnessTrackersPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
val vm: EndurainSetupViewModel by viewModels()
|
val vm: EndurainSetupViewModel by viewModels()
|
||||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
if (vm.tokenManager.isAccessTokenExpired()) {
|
if (vm.endurainTokenManager.isAccessTokenExpired()) {
|
||||||
vm.tokenManager.performTokenRefresh(server) {
|
vm.endurainTokenManager.performTokenRefresh(server) {
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
updateStatus()
|
updateStatus()
|
||||||
updateLogoutPreferenceVisibility()
|
updateLogoutPreferenceVisibility()
|
||||||
@@ -77,6 +77,16 @@ class OnlineFitnessTrackersPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
updateLogoutPreferenceVisibility()
|
updateLogoutPreferenceVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parentFragmentManager.setFragmentResultListener(
|
||||||
|
"wanderer_login_result",
|
||||||
|
this
|
||||||
|
) { _, bundle ->
|
||||||
|
val success = bundle.getBoolean("success", false)
|
||||||
|
if (success) {
|
||||||
|
updateStatus()
|
||||||
|
updateLogoutPreferenceVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -90,22 +100,21 @@ class OnlineFitnessTrackersPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
!GBApplication.hasInternetAccess()
|
!GBApplication.hasInternetAccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun wireLoginPreference() {
|
private fun wireLoginPreferences() {
|
||||||
findPreference<Preference>("pref_key_log_in")?.setOnPreferenceClickListener {
|
findPreference<Preference>("pref_key_endurain_log_in")?.setOnPreferenceClickListener {
|
||||||
EndurainSetupBottomSheet()
|
EndurainSetupBottomSheet()
|
||||||
.show(parentFragmentManager, "endurain_setup")
|
.show(parentFragmentManager, "endurain_setup")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
findPreference<Preference>("pref_key_wanderer_log_in")?.setOnPreferenceClickListener {
|
||||||
|
WandererSetupBottomSheet()
|
||||||
private fun wireLogoutPreference() {
|
.show(parentFragmentManager, "wanderer_setup")
|
||||||
findPreference<Preference>("pref_key_log_out")?.setOnPreferenceClickListener {
|
|
||||||
performLogout()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performLogout() {
|
private fun wireLogoutPreferences() {
|
||||||
|
findPreference<Preference>("pref_key_endurain_log_out")?.setOnPreferenceClickListener {
|
||||||
vm.logout { success ->
|
vm.logout { success ->
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -117,27 +126,53 @@ class OnlineFitnessTrackersPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
findPreference<Preference>("pref_key_wanderer_log_out")?.setOnPreferenceClickListener {
|
||||||
|
WandererTokenManager(requireContext()).clearTokens()
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
GB.toast(getString(R.string.endurain_logged_out_successfully), Toast.LENGTH_SHORT, GB.INFO)
|
||||||
|
updateStatus()
|
||||||
|
updateLogoutPreferenceVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLogoutPreferenceVisibility() {
|
private fun updateLogoutPreferenceVisibility() {
|
||||||
findPreference<Preference>("pref_key_log_out")?.isVisible = vm.tokenManager.isLoggedIn()
|
findPreference<Preference>("pref_key_endurain_log_out")?.isVisible = vm.endurainTokenManager.isLoggedIn()
|
||||||
findPreference<Preference>("pref_key_log_in")?.isVisible = !vm.tokenManager.isLoggedIn()
|
findPreference<Preference>("pref_key_endurain_log_in")?.isVisible = !vm.endurainTokenManager.isLoggedIn()
|
||||||
|
findPreference<Preference>("pref_key_wanderer_log_out")?.isVisible = WandererTokenManager(requireContext()).isLoggedIn()
|
||||||
|
findPreference<Preference>("pref_key_wanderer_log_in")?.isVisible = !WandererTokenManager(requireContext()).isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus() {
|
private fun updateStatus() {
|
||||||
val statusPref = findPreference<Preference>("pref_key_status")
|
val endurainStatusPref = findPreference<Preference>("pref_key_endurain_status")
|
||||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
val endurainServer = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||||
val tokenExpiresAt = DateTimeUtils.parseTimeStamp(vm.tokenManager.getRefreshTokenExpiresAt())
|
val endurainTokenExpiresAt = DateTimeUtils.parseTimeStamp(vm.endurainTokenManager.getRefreshTokenExpiresAt())
|
||||||
|
val wandererStatusPref = findPreference<Preference>("pref_key_wanderer_status")
|
||||||
|
val wandererServer = GBApplication.getPrefs().preferences.getString("wanderer_server", null)
|
||||||
|
val wandererAPITokenAvailable = WandererTokenManager(requireContext()).isLoggedIn()
|
||||||
|
|
||||||
|
// Update Endurain preferences
|
||||||
var summaryText = getString(R.string.endurain_not_logged_in_integration_disabled)
|
var summaryText = getString(R.string.endurain_not_logged_in_integration_disabled)
|
||||||
if (vm.tokenManager.isLoggedIn() && server != null) {
|
if (vm.endurainTokenManager.isLoggedIn() && endurainServer != null) {
|
||||||
summaryText =
|
summaryText =
|
||||||
getString(R.string.endurain_logged_in_refresh_token, server, tokenExpiresAt)
|
getString(R.string.endurain_logged_in_refresh_token, endurainServer, endurainTokenExpiresAt)
|
||||||
}
|
}
|
||||||
if (vm.serverVersion != null) {
|
if (vm.endurainServerVersion != null) {
|
||||||
summaryText += getString(R.string.endurain_server_version, vm.serverVersion)
|
summaryText += getString(R.string.endurain_server_version, vm.endurainServerVersion)
|
||||||
}
|
}
|
||||||
statusPref?.summary = summaryText
|
endurainStatusPref?.summary = summaryText
|
||||||
|
|
||||||
|
// Update Wanderer preferences
|
||||||
|
summaryText = getString(R.string.endurain_not_logged_in_integration_disabled)
|
||||||
|
if (wandererAPITokenAvailable) {
|
||||||
|
summaryText =
|
||||||
|
getString(R.string.wanderer_logged_in).format(wandererServer)
|
||||||
|
}
|
||||||
|
wandererStatusPref?.summary = summaryText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/* Copyright (C) 2026 Arjan Schrijver
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.endurain
|
||||||
|
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class WandererApiClient(
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val tokenManager: WandererTokenManager
|
||||||
|
) {
|
||||||
|
private val LOG = LoggerFactory.getLogger(WandererApiClient::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build headers with authentication tokens
|
||||||
|
*/
|
||||||
|
private fun buildHeaders(): MutableMap<String, String> {
|
||||||
|
val headers: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
|
tokenManager.getAPIToken()?.let { token ->
|
||||||
|
headers["Authorization"] = "Bearer $token"
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload activity file (GPX)
|
||||||
|
*/
|
||||||
|
fun uploadActivity(file: File, callback: (String?, String?) -> Unit) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val uri = "$baseUrl/api/v1/trail/upload".toUri()
|
||||||
|
val headers = buildHeaders()
|
||||||
|
|
||||||
|
InternetUtils.uploadBinaryFile(
|
||||||
|
uri = uri,
|
||||||
|
file = file,
|
||||||
|
requestHeaders = headers,
|
||||||
|
method = "PUT"
|
||||||
|
) { success, statusCode, responseText ->
|
||||||
|
if (success && statusCode != null && statusCode >= 200 && statusCode < 300 && responseText != null) {
|
||||||
|
LOG.debug("Response $statusCode from Wanderer: $responseText")
|
||||||
|
val jsonObject = JSONObject(responseText)
|
||||||
|
callback(jsonObject.getString("id"), null)
|
||||||
|
} else {
|
||||||
|
if (responseText != null) {
|
||||||
|
val jsonObject = JSONObject(responseText)
|
||||||
|
val message = jsonObject.getString("message")
|
||||||
|
LOG.error("Activity upload failed: $message")
|
||||||
|
callback(null, message)
|
||||||
|
} else {
|
||||||
|
LOG.error("Activity upload failed")
|
||||||
|
callback(null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LOG.error("Activity upload error", e)
|
||||||
|
callback(null, null)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/* Copyright (C) 2026 Arjan Schrijver
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.endurain
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class WandererSetupBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
private val LOG = LoggerFactory.getLogger(WandererSetupBottomSheet::class.java)
|
||||||
|
private val prefs get() = GBApplication.getPrefs().preferences
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = inflater.inflate(
|
||||||
|
R.layout.wanderer_bottomsheet_setup_wizard,
|
||||||
|
container,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val serverNameInput = view.findViewById<EditText>(R.id.wanderer_server_name)
|
||||||
|
val apiTokenInput = view.findViewById<EditText>(R.id.wanderer_api_token)
|
||||||
|
val saveButton = view.findViewById<Button>(R.id.save_button)
|
||||||
|
|
||||||
|
serverNameInput.setText(prefs.getString("wanderer_server", ""))
|
||||||
|
|
||||||
|
saveButton.setOnClickListener {
|
||||||
|
val serverName = serverNameInput.text.toString().trim()
|
||||||
|
val apiToken = apiTokenInput.text.toString().trim()
|
||||||
|
|
||||||
|
if (serverName.isNotEmpty() && apiToken.startsWith("wanderer_key_")) {
|
||||||
|
LOG.info("Saving Wanderer server ({}) and API token", serverName)
|
||||||
|
prefs.edit { putString("wanderer_server", serverName) }
|
||||||
|
WandererTokenManager(requireContext()).saveToken(apiToken)
|
||||||
|
parentFragmentManager.setFragmentResult(
|
||||||
|
"wanderer_login_result",
|
||||||
|
Bundle().apply { putBoolean("success", true) }
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
} else if (serverName.isNotEmpty() && apiToken.isNotEmpty()) {
|
||||||
|
GB.toast(getString(R.string.wanderer_setup_api_token_error), Toast.LENGTH_SHORT, GB.WARN)
|
||||||
|
} else {
|
||||||
|
GB.toast(getString(R.string.wanderer_setup_missing_information), Toast.LENGTH_SHORT, GB.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/* Copyright (C) 2026 Arjan Schrijver
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.endurain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKey
|
||||||
|
|
||||||
|
class WandererTokenManager(context: Context) {
|
||||||
|
private val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val tokenPreferences = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"wanderer_tokens",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveToken(apiToken: String) {
|
||||||
|
tokenPreferences.edit {
|
||||||
|
putString("api_token", apiToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearTokens() {
|
||||||
|
tokenPreferences.edit { clear() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAPIToken(): String? = tokenPreferences.getString("api_token", null)
|
||||||
|
|
||||||
|
fun isLoggedIn(): Boolean {
|
||||||
|
return getAPIToken() != null && getAPIToken()?.startsWith("wanderer_key_") == true
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-3
@@ -61,6 +61,8 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFra
|
|||||||
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.EndurainApiClient
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainSetupViewModel
|
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.EndurainSetupViewModel
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.WandererApiClient
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.WandererTokenManager
|
||||||
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
|
||||||
@@ -551,6 +553,11 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.activity_action_upload_to_wanderer -> {
|
||||||
|
uploadToWanderer()
|
||||||
|
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)
|
||||||
@@ -638,7 +645,8 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
|
|
||||||
val endurainVm: EndurainSetupViewModel by viewModels()
|
val endurainVm: EndurainSetupViewModel by viewModels()
|
||||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||||
overflowMenu?.findItem(R.id.activity_action_upload_to_endurain)?.isVisible = hasGpx && server != null && endurainVm.tokenManager.isLoggedIn()
|
overflowMenu?.findItem(R.id.activity_action_upload_to_endurain)?.isVisible = hasGpx && server != null && endurainVm.endurainTokenManager.isLoggedIn()
|
||||||
|
overflowMenu?.findItem(R.id.activity_action_upload_to_wanderer)?.isVisible = hasGpx && server != null && WandererTokenManager(requireContext()).isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun takeSharedScreenshot() {
|
private fun takeSharedScreenshot() {
|
||||||
@@ -765,8 +773,8 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
try {
|
try {
|
||||||
val endurainVm: EndurainSetupViewModel by viewModels()
|
val endurainVm: EndurainSetupViewModel by viewModels()
|
||||||
val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||||
val apiClient = EndurainApiClient(serverUrl!!, endurainVm.tokenManager)
|
val apiClient = EndurainApiClient(serverUrl!!, endurainVm.endurainTokenManager)
|
||||||
endurainVm.tokenManager.performTokenRefresh(serverUrl) {
|
endurainVm.endurainTokenManager.performTokenRefresh(serverUrl) {
|
||||||
LOG.info("Uploading workout '{}' (type {}) to Endurain", workoutName, activityKind)
|
LOG.info("Uploading workout '{}' (type {}) to Endurain", workoutName, activityKind)
|
||||||
apiClient.uploadActivity(activityFile) { newId ->
|
apiClient.uploadActivity(activityFile) { newId ->
|
||||||
if (newId != null) {
|
if (newId != null) {
|
||||||
@@ -799,6 +807,57 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun uploadToWanderer() {
|
||||||
|
val workout = currentWorkout ?: return
|
||||||
|
val workoutName = currentWorkout!!.summary.name
|
||||||
|
val activityKind = ActivityKind.fromCode(currentWorkout!!.summary.activityKind)
|
||||||
|
val activityTrackProvider = gbDevice.deviceCoordinator.getActivityTrackProvider(gbDevice, requireContext())
|
||||||
|
|
||||||
|
val activityFile = if (workout.summary.rawDetailsPath?.endsWith(".fit") == true) {
|
||||||
|
FileUtils.tryFixPath(workout.summary.rawDetailsPath)
|
||||||
|
} else {
|
||||||
|
ActivitySummaryUtils.getShareableGpxFile(activityTrackProvider, workout.summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activityFile == null) {
|
||||||
|
GB.toast(getString(R.string.no_activity_track_in_activity_toast), Toast.LENGTH_LONG, GB.INFO)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val serverUrl = GBApplication.getPrefs().preferences.getString("wanderer_server", null)
|
||||||
|
val apiClient = WandererApiClient(serverUrl!!, WandererTokenManager(requireContext()))
|
||||||
|
apiClient.uploadActivity(activityFile) { newId, message ->
|
||||||
|
if (newId != null && message == null) {
|
||||||
|
LOG.info("Uploaded GPX to Wanderer, ID $newId")
|
||||||
|
// TODO: Update activity type on the server
|
||||||
|
//apiClient.editActivity(newId, activityKind, workoutName)
|
||||||
|
}
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
if (newId != null && message == null)
|
||||||
|
GB.toast(
|
||||||
|
getString(R.string.wanderer_toast_successfully_uploaded),
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
GB.INFO
|
||||||
|
)
|
||||||
|
else
|
||||||
|
GB.toast(
|
||||||
|
getString(R.string.wanderer_toast_upload_error, message),
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
GB.INFO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
GB.toast(
|
||||||
|
getString(R.string.wanderer_unable_to_upload_gpx_file_toast, 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)
|
||||||
|
|||||||
@@ -154,13 +154,14 @@ class InternetUtils {
|
|||||||
uri: Uri,
|
uri: Uri,
|
||||||
file: File,
|
file: File,
|
||||||
requestHeaders: Map<String, String> = emptyMap(),
|
requestHeaders: Map<String, String> = emptyMap(),
|
||||||
|
method: String = "POST",
|
||||||
allowInsecure: Boolean = false,
|
allowInsecure: Boolean = false,
|
||||||
onComplete: (success: Boolean, response: String?) -> Unit
|
onComplete: (success: Boolean, statusCode: Int?, response: String?) -> Unit
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (!file.exists() || !file.canRead()) {
|
if (!file.exists() || !file.canRead()) {
|
||||||
LOG.error("File does not exist or cannot be read: ${file.path}")
|
LOG.error("File does not exist or cannot be read: ${file.path}")
|
||||||
onComplete(false, null)
|
onComplete(false, null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ class InternetUtils {
|
|||||||
val response = if (GBApplication.hasDirectInternetAccess()) {
|
val response = if (GBApplication.hasDirectInternetAccess()) {
|
||||||
directBinaryRequest(
|
directBinaryRequest(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
method = "POST",
|
method = method,
|
||||||
requestHeaders = headers,
|
requestHeaders = headers,
|
||||||
body = multipartBodyBytes,
|
body = multipartBodyBytes,
|
||||||
allowInsecure = allowInsecure
|
allowInsecure = allowInsecure
|
||||||
@@ -187,7 +188,7 @@ class InternetUtils {
|
|||||||
} else {
|
} else {
|
||||||
InternetHelperSingleton.send(
|
InternetHelperSingleton.send(
|
||||||
uri,
|
uri,
|
||||||
HttpRequest.Method.POST,
|
HttpRequest.Method.valueOf(method),
|
||||||
headers,
|
headers,
|
||||||
multipartBodyBytes,
|
multipartBodyBytes,
|
||||||
allowInsecure
|
allowInsecure
|
||||||
@@ -195,10 +196,10 @@ class InternetUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val responseText = response?.data?.bufferedReader()?.use { it.readText() }
|
val responseText = response?.data?.bufferedReader()?.use { it.readText() }
|
||||||
onComplete(response != null, responseText)
|
onComplete(response != null, response?.statusCode, responseText)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LOG.error("Uploading $uri failed: ", e)
|
LOG.error("Uploading $uri failed: ", e)
|
||||||
onComplete(false, null)
|
onComplete(false, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
android:id="@+id/server_layout"
|
android:id="@+id/server_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/endurain_setup_server">
|
android:hint="@string/endurain_setup_server"
|
||||||
|
app:placeholderText="https://endurain.example.com">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/server_input"
|
android:id="@+id/server_input"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
app:placeholderText="https://endurain.example.com"
|
||||||
android:inputType="textUri" />
|
android:inputType="textUri" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Wanderer setup"
|
||||||
|
android:textAppearance="?attr/textAppearanceHeadlineSmall" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Server hostname"
|
||||||
|
app:placeholderText="https://wanderer.example.com"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wanderer_server_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textUri"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="API token"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wanderer_api_token"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="12dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/save_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/save" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -56,6 +56,12 @@
|
|||||||
android:title="@string/activity_detail_upload_to_endurain"
|
android:title="@string/activity_detail_upload_to_endurain"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/activity_action_upload_to_wanderer"
|
||||||
|
android:icon="@drawable/ic_file_upload"
|
||||||
|
android:title="Upload GPX to Wanderer"
|
||||||
|
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"
|
||||||
|
|||||||
@@ -4983,4 +4983,14 @@
|
|||||||
<string name="endurain_setup_mfa_code">MFA code</string>
|
<string name="endurain_setup_mfa_code">MFA code</string>
|
||||||
<string name="endurain_setup_next">Next</string>
|
<string name="endurain_setup_next">Next</string>
|
||||||
<string name="activity_detail_upload_to_endurain">Upload to Endurain</string>
|
<string name="activity_detail_upload_to_endurain">Upload to Endurain</string>
|
||||||
|
<string name="wanderer_logged_in">Logged in to %s</string>
|
||||||
|
<string name="wanderer_setup_api_token_error">API token should start with wanderer_key_</string>
|
||||||
|
<string name="wanderer_setup_missing_information">Please fill both fields</string>
|
||||||
|
<string name="wanderer_toast_successfully_uploaded">Successfully uploaded to Wanderer</string>
|
||||||
|
<string name="wanderer_toast_upload_error">Wanderer upload error: %1$s</string>
|
||||||
|
<string name="wanderer_unable_to_upload_gpx_file_toast">Unable to upload GPX file to Wanderer: %1$s</string>
|
||||||
|
<string name="pref_wanderer_status_title">Wanderer status</string>
|
||||||
|
<string name="pref_wanderer_enter_api_token_title">Enter Wanderer API token</string>
|
||||||
|
<string name="pref_wanderer_enter_api_token_summary">Tap here to enter the Wanderer server and API token</string>
|
||||||
|
<string name="pref_wanderer_log_out_title">Log out of Wanderer</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -10,18 +10,36 @@
|
|||||||
android:title="Endurain">
|
android:title="Endurain">
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_info"
|
android:icon="@drawable/ic_info"
|
||||||
android:key="pref_key_status"
|
android:key="pref_key_endurain_status"
|
||||||
android:title="@string/pref_info_endurain_status_title"
|
android:title="@string/pref_info_endurain_status_title"
|
||||||
android:summary="Logged in on endurain.example.com" />
|
android:summary="Logged in on endurain.example.com" />
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_language"
|
android:icon="@drawable/ic_language"
|
||||||
android:key="pref_key_log_in"
|
android:key="pref_key_endurain_log_in"
|
||||||
android:title="@string/pref_endurain_log_in_title"
|
android:title="@string/pref_endurain_log_in_title"
|
||||||
android:summary="@string/pref_endurain_log_in_summary" />
|
android:summary="@string/pref_endurain_log_in_summary" />
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_language"
|
android:icon="@drawable/ic_language"
|
||||||
android:key="pref_key_log_out"
|
android:key="pref_key_endurain_log_out"
|
||||||
android:title="@string/pref_endurain_log_out_title"
|
android:title="@string/pref_endurain_log_out_title"
|
||||||
android:summary="@string/pref_endurain_log_out_summary" />
|
android:summary="@string/pref_endurain_log_out_summary" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
<PreferenceCategory
|
||||||
|
android:title="Wanderer">
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_info"
|
||||||
|
android:key="pref_key_wanderer_status"
|
||||||
|
android:title="@string/pref_wanderer_status_title"
|
||||||
|
android:summary="Logged in on wanderer.example.com" />
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_language"
|
||||||
|
android:key="pref_key_wanderer_log_in"
|
||||||
|
android:title="@string/pref_wanderer_enter_api_token_title"
|
||||||
|
android:summary="@string/pref_wanderer_enter_api_token_summary" />
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_language"
|
||||||
|
android:key="pref_key_wanderer_log_out"
|
||||||
|
android:title="@string/pref_wanderer_log_out_title"
|
||||||
|
android:summary="@string/pref_endurain_log_out_summary" />
|
||||||
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
Reference in New Issue
Block a user