Endurain: Improve token refresh logic

This commit is contained in:
Arjan Schrijver
2026-06-19 23:31:23 +02:00
parent df89cf8bb6
commit 5dfa20202d
4 changed files with 59 additions and 52 deletions
@@ -25,6 +25,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2 import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
import nodomain.freeyourgadget.gadgetbridge.util.GB
class EndurainPreferencesActivity : AbstractSettingsActivityV2() { class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
@@ -45,14 +47,16 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
updateLogoutPreferenceVisibility() updateLogoutPreferenceVisibility()
setupLoginResultListener() setupLoginResultListener()
// Refresh auth token // Refresh tokens
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) {
vm.performTokenRefresh(server) { if (vm.tokenManager.isAccessTokenExpired()) {
activity?.runOnUiThread { vm.performTokenRefresh(server) {
updateStatus() activity?.runOnUiThread {
updateLogoutPreferenceVisibility() updateStatus()
updateLogoutPreferenceVisibility()
}
} }
} }
vm.fetchServerVersion(server) { vm.fetchServerVersion(server) {
@@ -106,29 +110,29 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
vm.logout { success -> vm.logout { success ->
activity?.runOnUiThread { activity?.runOnUiThread {
if (success) { if (success) {
Toast.makeText(requireContext(), "Logged out successfully", Toast.LENGTH_SHORT).show() GB.toast("Logged out successfully", Toast.LENGTH_SHORT, GB.INFO)
updateStatus() updateStatus()
updateLogoutPreferenceVisibility() updateLogoutPreferenceVisibility()
} else { } else {
Toast.makeText(requireContext(), "Logout failed", Toast.LENGTH_SHORT).show() GB.toast("Logout failed", Toast.LENGTH_SHORT, GB.WARN)
} }
} }
} }
} }
private fun updateLogoutPreferenceVisibility() { private fun updateLogoutPreferenceVisibility() {
findPreference<Preference>("pref_key_log_out")?.isVisible = vm.isLoggedIn() findPreference<Preference>("pref_key_log_out")?.isVisible = vm.tokenManager.isLoggedIn()
findPreference<Preference>("pref_key_log_in")?.isVisible = !vm.isLoggedIn() findPreference<Preference>("pref_key_log_in")?.isVisible = !vm.tokenManager.isLoggedIn()
} }
private fun updateStatus() { private fun updateStatus() {
val statusPref = findPreference<Preference>("pref_key_status") val statusPref = findPreference<Preference>("pref_key_status")
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null) val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
val tokenExpiresAt = vm.getTokenExpiresAt() val tokenExpiresAt = DateTimeUtils.parseTimeStamp(vm.tokenManager.getRefreshTokenExpiresAt())
var summaryText = "Not logged in, integration is disabled" var summaryText = "Not logged in, integration is disabled"
if (vm.isLoggedIn() && server != null) { if (vm.tokenManager.isLoggedIn() && server != null) {
summaryText = "Logged in to $server\nAuth token expires: $tokenExpiresAt" summaryText = "Logged in to $server\nRefresh token expires: $tokenExpiresAt"
} }
if (vm.serverVersion != null) { if (vm.serverVersion != null) {
summaryText += "\nServer version: ${vm.serverVersion}" summaryText += "\nServer version: ${vm.serverVersion}"
@@ -27,7 +27,6 @@ import java.util.Date
class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) { class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) {
private val LOG = LoggerFactory.getLogger(EndurainSetupViewModel::class.java) private val LOG = LoggerFactory.getLogger(EndurainSetupViewModel::class.java)
private val tokenManager = EndurainTokenManager(application)
private lateinit var apiClient: EndurainApiClient private lateinit var apiClient: EndurainApiClient
enum class Step { enum class Step {
@@ -38,6 +37,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
SSO_LOGIN SSO_LOGIN
} }
val tokenManager = EndurainTokenManager(application)
var step = Step.SERVER var step = Step.SERVER
var server = "" var server = ""
var localLoginEnabled = false var localLoginEnabled = false
@@ -45,7 +45,6 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
var pendingMfaUsername: String? = null var pendingMfaUsername: String? = null
var serverVersion: String? = null var serverVersion: String? = null
/** /**
* Fetch server version * Fetch server version
*/ */
@@ -57,9 +56,10 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
try { try {
apiClient = EndurainApiClient(serverUrl, tokenManager) apiClient = EndurainApiClient(serverUrl, tokenManager)
serverVersion = apiClient.fetchVersion() serverVersion = apiClient.fetchVersion()
callback(true)
} catch (e: Exception) { } catch (e: Exception) {
LOG.error("Fetching server version error", e) LOG.error("Fetching server version error", e)
callback(true) callback(false)
} }
}.start() }.start()
} }
@@ -224,20 +224,6 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
}.start() }.start()
} }
/**
* Check if user is currently logged in
*/
fun isLoggedIn(): Boolean {
return tokenManager.getAccessToken() != null && !tokenManager.isTokenExpired()
}
/**
* Get token expiry date
*/
fun getTokenExpiresAt(): Date {
return DateTimeUtils.parseTimeStamp(tokenManager.getAccessTokenExpiresAt())
}
/** /**
* Logout and clear tokens * Logout and clear tokens
*/ */
@@ -38,18 +38,30 @@ class EndurainTokenManager(context: Context) {
sharedPreferences.edit { sharedPreferences.edit {
putString("access_token", accessToken) putString("access_token", accessToken)
.putString("refresh_token", refreshToken) .putString("refresh_token", refreshToken)
.putInt("expires_at", accessTokenExpiresAt) .putInt("access_token_expires_at", accessTokenExpiresAt)
.putInt("refresh_token_expires_at",
((System.currentTimeMillis() / 1000) + (7 * 24 * 60 * 60)).toInt()
) // FIXME: 7 days is the Endurain default for refresh token expiry
// https://github.com/endurain-project/endurain/issues/514
} }
} }
fun getAccessToken(): String? = sharedPreferences.getString("access_token", null)
fun getRefreshToken(): String? = sharedPreferences.getString("refresh_token", null)
fun getAccessTokenExpiresAt(): Int = sharedPreferences.getInt("expires_at", 0)
fun isTokenExpired(): Boolean {
return (System.currentTimeMillis() / 1000) >= getAccessTokenExpiresAt()
}
fun clearTokens() { fun clearTokens() {
sharedPreferences.edit { clear() } sharedPreferences.edit { clear() }
} }
fun getAccessToken(): String? = sharedPreferences.getString("access_token", null)
fun getAccessTokenExpiresAt(): Int = sharedPreferences.getInt("access_token_expires_at", 0)
fun getRefreshToken(): String? = sharedPreferences.getString("refresh_token", null)
fun getRefreshTokenExpiresAt(): Int = sharedPreferences.getInt("refresh_token_expires_at", 0)
fun isLoggedIn(): Boolean {
return getRefreshToken() != null && !isRefreshTokenExpired()
}
fun isAccessTokenExpired(): Boolean {
return (System.currentTimeMillis() / 1000) >= getAccessTokenExpiresAt()
}
fun isRefreshTokenExpired(): Boolean {
return (System.currentTimeMillis() / 1000) >= getRefreshTokenExpiresAt()
}
} }
@@ -61,7 +61,6 @@ 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.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
@@ -639,13 +638,7 @@ 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)
if (server != null) { overflowMenu?.findItem(R.id.activity_action_upload_to_endurain)?.isVisible = hasGpx && server != null && endurainVm.tokenManager.isLoggedIn()
endurainVm.performTokenRefresh(server) {
activity?.runOnUiThread {
overflowMenu?.findItem(R.id.activity_action_upload_to_endurain)?.isVisible = hasGpx && endurainVm.isLoggedIn()
}
}
}
} }
private fun takeSharedScreenshot() { private fun takeSharedScreenshot() {
@@ -761,14 +754,26 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
} }
try { try {
val tokenManager = EndurainTokenManager(requireContext()) 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!!, tokenManager) val apiClient = EndurainApiClient(serverUrl!!, endurainVm.tokenManager)
apiClient.uploadActivity(gpxFile) { success -> endurainVm.performTokenRefresh(serverUrl) {
if (success) apiClient.uploadActivity(gpxFile) { success ->
GB.toast("Successfully uploaded to Endurain", Toast.LENGTH_SHORT, GB.INFO) activity?.runOnUiThread {
else if (success)
GB.toast("Error while uploading to Endurain", Toast.LENGTH_SHORT, GB.INFO) 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) { } catch (e: Exception) {
GB.toast( GB.toast(