diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
index abb083403f..5a51340419 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java
@@ -70,6 +70,7 @@ import java.util.Locale;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
+import nodomain.freeyourgadget.gadgetbridge.activities.endurain.PeriodicEndurainTokenRefresher;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter;
@@ -290,6 +291,11 @@ public class GBApplication extends Application {
// Ensure the InternetHelper is bound, so that it works on first usage
InternetHelperSingleton.INSTANCE.ensureInternetHelperBound();
+
+ // Refresh Endurain tokens and schedule periodic refresh
+ if (!GBEnvironment.env().isTest()) {
+ PeriodicEndurainTokenRefresher.INSTANCE.scheduleNextExecution(context);
+ }
}
private void startNotificationCollectorMonitorService() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt
index 46496f12d7..25ca7ed7a9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainPreferencesActivity.kt
@@ -29,7 +29,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
import nodomain.freeyourgadget.gadgetbridge.util.GB
class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
-
override fun newFragment(): PreferenceFragmentCompat =
EndurainPreferencesFragment()
@@ -52,7 +51,7 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
if (server != null) {
if (vm.tokenManager.isAccessTokenExpired()) {
- vm.performTokenRefresh(server) {
+ vm.tokenManager.performTokenRefresh(server) {
activity?.runOnUiThread {
updateStatus()
updateLogoutPreferenceVisibility()
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt
index 612de43b54..f8b119c3e9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupViewModel.kt
@@ -19,10 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.endurain
import android.app.Application
import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
-import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
import org.slf4j.LoggerFactory
-import java.util.Date
class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) {
@@ -105,44 +103,6 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
}.start()
}
- /**
- * Perform token refresh
- */
- fun performTokenRefresh(
- serverUrl: String,
- callback: (Boolean) -> Unit
- ) {
- Thread {
- try {
- apiClient = EndurainApiClient(serverUrl, tokenManager)
- val response = apiClient.refreshToken()
-
- when {
- response == null -> {
- LOG.error("Token refresh failed: null response")
- callback(false)
- }
- response.access_token != null -> {
- LOG.info("Token refresh successful")
- tokenManager.saveTokens(
- response.access_token,
- response.refresh_token!!,
- response.expires_in!!
- )
- callback(true)
- }
- else -> {
- LOG.error("Token refresh failed: ${response.message}")
- callback(false)
- }
- }
- } catch (e: Exception) {
- LOG.error("Token refresh error", e)
- callback(false)
- }
- }.start()
- }
-
/**
* Perform local username/password login
*/
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenManager.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenManager.kt
index 82d1d26aae..308500d179 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenManager.kt
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenManager.kt
@@ -20,8 +20,11 @@ import android.content.Context
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
class EndurainTokenManager(context: Context) {
+ private val LOG: Logger = LoggerFactory.getLogger(EndurainTokenManager::class.java)
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
@@ -64,4 +67,39 @@ class EndurainTokenManager(context: Context) {
fun isRefreshTokenExpired(): Boolean {
return (System.currentTimeMillis() / 1000) >= getRefreshTokenExpiresAt()
}
+
+ fun performTokenRefresh(
+ serverUrl: String,
+ callback: (Boolean) -> Unit
+ ) {
+ Thread {
+ try {
+ val apiClient = EndurainApiClient(serverUrl, this)
+ val response = apiClient.refreshToken()
+
+ when {
+ response == null -> {
+ LOG.error("Token refresh failed: null response")
+ callback(false)
+ }
+ response.access_token != null -> {
+ LOG.info("Token refresh successful")
+ saveTokens(
+ response.access_token,
+ response.refresh_token!!,
+ response.expires_in!!
+ )
+ callback(true)
+ }
+ else -> {
+ LOG.error("Token refresh failed: ${response.detail}")
+ callback(false)
+ }
+ }
+ } catch (e: Exception) {
+ LOG.error("Token refresh error", e)
+ callback(false)
+ }
+ }.start()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenRefreshWorker.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenRefreshWorker.kt
new file mode 100644
index 0000000000..8ee3e21e17
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainTokenRefreshWorker.kt
@@ -0,0 +1,45 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.activities.endurain
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import nodomain.freeyourgadget.gadgetbridge.GBApplication
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+class EndurainTokenRefreshWorker (
+ context: Context,
+ params: WorkerParameters
+) : Worker(context, params) {
+
+ private val LOG: Logger = LoggerFactory.getLogger(EndurainTokenRefreshWorker::class.java)
+
+ override fun doWork(): Result {
+ LOG.info("Running Endurain token refresh worker")
+
+ // Refresh tokens if possible
+ val tokenManager = EndurainTokenManager(applicationContext)
+ val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null)
+ if (serverUrl != null && tokenManager.isLoggedIn()) {
+ tokenManager.performTokenRefresh(serverUrl) {}
+ }
+
+ return Result.success()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/PeriodicEndurainTokenRefresher.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/PeriodicEndurainTokenRefresher.kt
new file mode 100644
index 0000000000..a2478a820f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/PeriodicEndurainTokenRefresher.kt
@@ -0,0 +1,63 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.activities.endurain
+
+import android.content.Context
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.util.concurrent.TimeUnit
+
+object PeriodicEndurainTokenRefresher {
+ private val LOG: Logger = LoggerFactory.getLogger(PeriodicEndurainTokenRefresher::class.java)
+
+ const val TAG_CREATED_AT = "createdAt-"
+ const val WORK_TAG = "EndurainTokenRefreshWorker"
+
+ fun scheduleNextExecution(context: Context) {
+ try {
+ val tokenManager = EndurainTokenManager(context)
+ val workManager = WorkManager.getInstance(context)
+ workManager.cancelAllWorkByTag(WORK_TAG)
+
+ if (!tokenManager.isLoggedIn()) {
+ LOG.info("Not scheduling {}, no valid refresh token available", this::class.java.simpleName)
+ return
+ }
+
+ // Runs every 3 days
+ val periodicWork = PeriodicWorkRequestBuilder(
+ 3, TimeUnit.DAYS
+ )
+ .addTag(WORK_TAG)
+ .addTag("$TAG_CREATED_AT${System.currentTimeMillis()}")
+ .build()
+
+ WorkManager.getInstance(context).apply {
+ enqueueUniquePeriodicWork(
+ WORK_TAG,
+ ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
+ periodicWork
+ )
+ }
+ } catch (e: Exception) {
+ LOG.error("Failed to schedule next execution for {}", this::class.java.simpleName, e)
+ }
+ }
+}
\ No newline at end of file
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 18dbb42c2c..32ebc69ed8 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
@@ -757,7 +757,7 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
val endurainVm: EndurainSetupViewModel by viewModels()
val serverUrl = GBApplication.getPrefs().preferences.getString("endurain_server", null)
val apiClient = EndurainApiClient(serverUrl!!, endurainVm.tokenManager)
- endurainVm.performTokenRefresh(serverUrl) {
+ endurainVm.tokenManager.performTokenRefresh(serverUrl) {
apiClient.uploadActivity(gpxFile) { success ->
activity?.runOnUiThread {
if (success)