mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Endurain: Refresh tokens periodically
This commit is contained in:
@@ -70,6 +70,7 @@ import java.util.Locale;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.endurain.PeriodicEndurainTokenRefresher;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter;
|
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
|
// Ensure the InternetHelper is bound, so that it works on first usage
|
||||||
InternetHelperSingleton.INSTANCE.ensureInternetHelperBound();
|
InternetHelperSingleton.INSTANCE.ensureInternetHelperBound();
|
||||||
|
|
||||||
|
// Refresh Endurain tokens and schedule periodic refresh
|
||||||
|
if (!GBEnvironment.env().isTest()) {
|
||||||
|
PeriodicEndurainTokenRefresher.INSTANCE.scheduleNextExecution(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startNotificationCollectorMonitorService() {
|
private void startNotificationCollectorMonitorService() {
|
||||||
|
|||||||
+1
-2
@@ -29,7 +29,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||||
|
|
||||||
class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
|
class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
|
||||||
|
|
||||||
override fun newFragment(): PreferenceFragmentCompat =
|
override fun newFragment(): PreferenceFragmentCompat =
|
||||||
EndurainPreferencesFragment()
|
EndurainPreferencesFragment()
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
|
|||||||
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.tokenManager.isAccessTokenExpired()) {
|
||||||
vm.performTokenRefresh(server) {
|
vm.tokenManager.performTokenRefresh(server) {
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
updateStatus()
|
updateStatus()
|
||||||
updateLogoutPreferenceVisibility()
|
updateLogoutPreferenceVisibility()
|
||||||
|
|||||||
-40
@@ -19,10 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.endurain
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) {
|
class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
@@ -105,44 +103,6 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
}.start()
|
}.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
|
* Perform local username/password login
|
||||||
*/
|
*/
|
||||||
|
|||||||
+38
@@ -20,8 +20,11 @@ import android.content.Context
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class EndurainTokenManager(context: Context) {
|
class EndurainTokenManager(context: Context) {
|
||||||
|
private val LOG: Logger = LoggerFactory.getLogger(EndurainTokenManager::class.java)
|
||||||
private val masterKey = MasterKey.Builder(context)
|
private val masterKey = MasterKey.Builder(context)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
@@ -64,4 +67,39 @@ class EndurainTokenManager(context: Context) {
|
|||||||
fun isRefreshTokenExpired(): Boolean {
|
fun isRefreshTokenExpired(): Boolean {
|
||||||
return (System.currentTimeMillis() / 1000) >= getRefreshTokenExpiresAt()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+45
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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<EndurainTokenRefreshWorker>(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -757,7 +757,7 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
|||||||
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.tokenManager)
|
||||||
endurainVm.performTokenRefresh(serverUrl) {
|
endurainVm.tokenManager.performTokenRefresh(serverUrl) {
|
||||||
apiClient.uploadActivity(gpxFile) { success ->
|
apiClient.uploadActivity(gpxFile) { success ->
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
if (success)
|
if (success)
|
||||||
|
|||||||
Reference in New Issue
Block a user