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)