diff --git a/app/build.gradle b/app/build.gradle index e810198cd8..6856ae348c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,6 +262,7 @@ dependencies { implementation libs.androidx.work.runtime.ktx implementation libs.androidx.lifecycle.process implementation libs.androidx.security.crypto + implementation libs.androidx.browser implementation libs.material implementation libs.flexbox diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bf910edcb3..eb3dbb2073 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1124,6 +1124,23 @@ + + + + + + + + + + + ? { + try { + val uri = "$baseUrl/api/v1/public/idp".toUri() + + val responseText = InternetUtils.doStringRequest(uri = uri) + + return if (responseText != null) { + val type = object : com.google.gson.reflect.TypeToken>() {}.type + gson.fromJson(responseText, type) + } else { + LOG.error("Failed to fetch identity providers") + null + } + } catch (e: Exception) { + LOG.error("Error fetching identity providers", e) + return null + } + } + /** * Exchange OAuth session for tokens (PKCE flow) */ @@ -220,6 +249,8 @@ class EndurainApiClient( body = body ) + LOG.debug("OAuth token result: $responseText") + return if (responseText != null) { gson.fromJson(responseText, LoginResponse::class.java) } else { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainOAuthCallbackActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainOAuthCallbackActivity.kt new file mode 100644 index 0000000000..082c014594 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainOAuthCallbackActivity.kt @@ -0,0 +1,84 @@ +/* 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.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import nodomain.freeyourgadget.gadgetbridge.R +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity +import org.slf4j.LoggerFactory + +/** + * Handles OAuth callback deep links from the browser + */ +class EndurainOAuthCallbackActivity : AbstractGBActivity() { + + private val LOG = LoggerFactory.getLogger(EndurainOAuthCallbackActivity::class.java) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Bring us back to front to dismiss the custom tab + val dismissIntent = Intent(applicationContext, EndurainPreferencesActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + startActivity(dismissIntent) + + // Handle OAuth callback intent + val uri = intent.data + if (uri != null) { + LOG.info("OAuth callback received: $uri") + handleOAuthCallback(uri) + } else { + LOG.error("No URI in OAuth callback") + Toast.makeText(this, + getString(R.string.endurain_invalid_oauth_callback), Toast.LENGTH_SHORT).show() + finish() + } + } + + private fun handleOAuthCallback(uri: Uri) { + // Expected format: nodomain.freeyourgadget.gadgetbridge://endurain/oauth/callback?session_id={uuid} + + val sessionId = uri.getQueryParameter("session_id") + + if (sessionId != null) { + LOG.info("SSO successful, session_id: $sessionId") + + // Broadcast the session ID so the setup bottom sheet can handle it + val resultIntent = Intent("nodomain.freeyourgadget.gadgetbridge.ENDURAIN_SSO_CALLBACK") + resultIntent.putExtra("session_id", sessionId) + resultIntent.putExtra("success", true) + sendBroadcast(resultIntent) + + Toast.makeText(this, + getString(R.string.endurain_oauth_completing_login), Toast.LENGTH_SHORT).show() + } else { + LOG.error("SSO failed or missing session_id") + Toast.makeText(this, getString(R.string.endurain_sso_login_failed), Toast.LENGTH_SHORT).show() + + // Broadcast failure + val resultIntent = Intent("nodomain.freeyourgadget.gadgetbridge.ENDURAIN_SSO_CALLBACK") + resultIntent.putExtra("success", false) + sendBroadcast(resultIntent) + } + + finish() + } +} \ No newline at end of file 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 25ca7ed7a9..ae750d35ba 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 @@ -109,11 +109,11 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() { vm.logout { success -> activity?.runOnUiThread { if (success) { - GB.toast("Logged out successfully", Toast.LENGTH_SHORT, GB.INFO) + GB.toast(getString(R.string.endurain_logged_out_successfully), Toast.LENGTH_SHORT, GB.INFO) updateStatus() updateLogoutPreferenceVisibility() } else { - GB.toast("Logout failed", Toast.LENGTH_SHORT, GB.WARN) + GB.toast(getString(R.string.endurain_logout_failed), Toast.LENGTH_SHORT, GB.WARN) } } } @@ -129,12 +129,13 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() { val server = GBApplication.getPrefs().preferences.getString("endurain_server", null) val tokenExpiresAt = DateTimeUtils.parseTimeStamp(vm.tokenManager.getRefreshTokenExpiresAt()) - var summaryText = "Not logged in, integration is disabled" + var summaryText = getString(R.string.endurain_not_logged_in_integration_disabled) if (vm.tokenManager.isLoggedIn() && server != null) { - summaryText = "Logged in to $server\nRefresh token expires: $tokenExpiresAt" + summaryText = + getString(R.string.endurain_logged_in_refresh_token, server, tokenExpiresAt) } if (vm.serverVersion != null) { - summaryText += "\nServer version: ${vm.serverVersion}" + summaryText += getString(R.string.endurain_server_version, vm.serverVersion) } statusPref?.summary = summaryText } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt index 022c1b6d9d..a138d65da8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSetupBottomSheet.kt @@ -16,11 +16,17 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.endurain +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.net.toUri import androidx.fragment.app.viewModels @@ -32,9 +38,11 @@ import com.google.android.material.textfield.TextInputLayout import nodomain.freeyourgadget.gadgetbridge.GBApplication import nodomain.freeyourgadget.gadgetbridge.R import nodomain.freeyourgadget.gadgetbridge.util.GB +import org.slf4j.LoggerFactory class EndurainSetupBottomSheet : BottomSheetDialogFragment() { + private val LOG = LoggerFactory.getLogger(EndurainSetupBottomSheet::class.java) private val prefs get() = GBApplication.getPrefs().preferences private val vm: EndurainSetupViewModel by viewModels() @@ -52,6 +60,24 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { private lateinit var progress: View private lateinit var next: MaterialButton + // Broadcast receiver for SSO callback + private val ssoCallbackReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val sessionId = intent?.getStringExtra("session_id") + val success = intent?.getBooleanExtra("success", true) ?: true + + if (sessionId != null && success) { + handleSsoCallback(sessionId) + } else { + activity?.runOnUiThread { + showProgress(false) + Toast.makeText(requireContext(), + getString(R.string.endurain_sso_login_failed), Toast.LENGTH_SHORT).show() + } + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -63,26 +89,31 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // Initialize views serverLayout = view.findViewById(R.id.server_layout) serverInput = view.findViewById(R.id.server_input) - loginTypeGroup = view.findViewById(R.id.login_type_group) localButton = view.findViewById(R.id.local_login_button) ssoButton = view.findViewById(R.id.sso_login_button) - userLayout = view.findViewById(R.id.user_layout) passLayout = view.findViewById(R.id.password_layout) userInput = view.findViewById(R.id.user_input) passInput = view.findViewById(R.id.password_input) - mfaLayout = view.findViewById(R.id.mfa_layout) mfaInput = view.findViewById(R.id.mfa_input) - progress = view.findViewById(R.id.progress) next = view.findViewById(R.id.next_button) serverInput.setText(prefs.getString("endurain_server", "")) + // Register broadcast receiver for SSO callbacks + ContextCompat.registerReceiver( + requireContext(), + ssoCallbackReceiver, + IntentFilter("nodomain.freeyourgadget.gadgetbridge.ENDURAIN_SSO_CALLBACK"), + ContextCompat.RECEIVER_EXPORTED + ) + next.setOnClickListener { when (vm.step) { EndurainSetupViewModel.Step.SERVER -> handleServerStep() @@ -102,10 +133,95 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { mfaLayout.visibility = View.GONE } R.id.sso_login_button -> { - vm.step = EndurainSetupViewModel.Step.SSO_LOGIN - // TODO: Launch SSO flow - GB.toast("SSO not yet implemented", Toast.LENGTH_SHORT, GB.INFO) + vm.step = EndurainSetupViewModel.Step.SSO_PROVIDERS + startSsoFlow() + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + try { + requireContext().unregisterReceiver(ssoCallbackReceiver) + } catch (e: Exception) { + // Already unregistered + } + } + + private fun startSsoFlow() { + showProgress(true) + vm.fetchSsoProviders { success -> + activity?.runOnUiThread { + showProgress(false) + if (success && vm.availableProviders.size == 1) { + launchSsoLogin(vm.availableProviders[0]) + } else if (success && vm.availableProviders.isNotEmpty()) { + showProviderSelection() + } else { + Toast.makeText( + requireContext(), + getString(R.string.endurain_no_sso_providers_available), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + private fun showProviderSelection() { + EndurainSsoProviderDialog(requireContext(), vm.availableProviders) { provider -> + launchSsoLogin(provider) + }.show() + } + + private fun launchSsoLogin(provider: IdentityProvider) { + val ssoUrl = vm.generateSsoUrl(provider.slug) + + LOG.info("Launching secure browser for SSO URL: $ssoUrl") + + // Launch Custom Tab (secure browser) + val customTabsIntent = CustomTabsIntent.Builder() + .setShowTitle(true) + .setUrlBarHidingEnabled(false) + .build() + + showProgress(true) + + try { + customTabsIntent.launchUrl(requireContext(), ssoUrl.toUri()) + } catch (e: Exception) { + showProgress(false) + Toast.makeText( + requireContext(), + getString(R.string.endurain_failed_to_open_browser, e.message), + Toast.LENGTH_SHORT + ).show() + } + } + + private fun handleSsoCallback(sessionId: String) { + showProgress(true) + vm.exchangeSsoSession(sessionId) { success -> + activity?.runOnUiThread { + showProgress(false) + if (success) { + Toast.makeText( + requireContext(), + getString(R.string.endurain_sso_login_successful), + Toast.LENGTH_SHORT + ).show() + parentFragmentManager.setFragmentResult( + "endurain_login_result", + Bundle().apply { putBoolean("success", true) } + ) dismiss() + } else { + Toast.makeText( + requireContext(), + getString(R.string.endurain_sso_login_failed), + Toast.LENGTH_SHORT + ).show() } } } @@ -114,7 +230,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { private fun handleServerStep() { val uri = serverInput.text.toString().toUri() if (uri.scheme == null || uri.host == null) { - serverLayout.error = "Invalid server URL" + serverLayout.error = getString(R.string.endurain_invalid_server_url) return } serverLayout.error = null @@ -127,7 +243,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { activity?.runOnUiThread { showProgress(false) if (!ok) { - GB.toast("Failed to connect to server", Toast.LENGTH_SHORT, GB.INFO) + GB.toast(getString(R.string.endurain_failed_to_connect_to_server), Toast.LENGTH_SHORT, GB.INFO) return@runOnUiThread } vm.step = EndurainSetupViewModel.Step.LOGIN_TYPE @@ -151,14 +267,14 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { var hasError = false if (user.isBlank()) { - userLayout.error = "Required" + userLayout.error = getString(R.string.required) hasError = true } else { userLayout.error = null } if (pass.isBlank()) { - passLayout.error = "Required" + passLayout.error = getString(R.string.required) hasError = true } else { passLayout.error = null @@ -176,11 +292,11 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { userLayout.visibility = View.GONE passLayout.visibility = View.GONE mfaLayout.visibility = View.VISIBLE - next.text = "Verify MFA" - GB.toast("Enter your MFA code", Toast.LENGTH_SHORT, GB.INFO) + next.text = getString(R.string.endurain_verify_mfa) + GB.toast(getString(R.string.endurain_enter_your_mfa_code), Toast.LENGTH_SHORT, GB.INFO) } success -> { - GB.toast("Login successful", Toast.LENGTH_SHORT, GB.INFO) + GB.toast(getString(R.string.endurain_login_successful), Toast.LENGTH_SHORT, GB.INFO) // Send result to parent fragment parentFragmentManager.setFragmentResult( "endurain_login_result", @@ -189,7 +305,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { dismiss() } else -> { - GB.toast("Login failed", Toast.LENGTH_SHORT, GB.INFO) + GB.toast(getString(R.string.endurain_login_failed), Toast.LENGTH_SHORT, GB.INFO) } } } @@ -199,7 +315,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { private fun handleMfaStep() { val mfaCode = mfaInput.text.toString() if (mfaCode.isBlank()) { - mfaLayout.error = "Required" + mfaLayout.error = getString(R.string.required) return } mfaLayout.error = null @@ -209,7 +325,7 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { activity?.runOnUiThread { showProgress(false) if (success) { - GB.toast("MFA verification successful", Toast.LENGTH_SHORT, GB.INFO) + GB.toast(getString(R.string.endurain_mfa_verification_successful), Toast.LENGTH_SHORT, GB.INFO) // Send result to parent fragment parentFragmentManager.setFragmentResult( "endurain_login_result", @@ -217,8 +333,8 @@ class EndurainSetupBottomSheet : BottomSheetDialogFragment() { ) dismiss() } else { - mfaLayout.error = "Invalid MFA code" - GB.toast("MFA verification failed", Toast.LENGTH_SHORT, GB.INFO) + mfaLayout.error = getString(R.string.endurain_invalid_mfa_code) + GB.toast(getString(R.string.endurain_mfa_verification_failed), Toast.LENGTH_SHORT, GB.INFO) } } } 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 f8b119c3e9..a068acb4f9 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 @@ -17,8 +17,12 @@ package nodomain.freeyourgadget.gadgetbridge.activities.endurain import android.app.Application +import android.content.Context +import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.AndroidViewModel +import nodomain.freeyourgadget.gadgetbridge.BuildConfig +import nodomain.freeyourgadget.gadgetbridge.GBApplication import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils import org.slf4j.LoggerFactory @@ -26,13 +30,14 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic private val LOG = LoggerFactory.getLogger(EndurainSetupViewModel::class.java) private lateinit var apiClient: EndurainApiClient + private val pkceHelper = PkceHelper() enum class Step { SERVER, LOGIN_TYPE, LOCAL_LOGIN, MFA_VERIFY, - SSO_LOGIN + SSO_PROVIDERS } val tokenManager = EndurainTokenManager(application) @@ -41,6 +46,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic var localLoginEnabled = false var ssoEnabled = false var pendingMfaUsername: String? = null + var availableProviders: List = emptyList() var serverVersion: String? = null /** @@ -103,6 +109,101 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic }.start() } + /** + * Fetch available SSO providers + */ + fun fetchSsoProviders(callback: (Boolean) -> Unit) { + Thread { + try { + apiClient = EndurainApiClient(server, tokenManager) + val providers = apiClient.getIdentityProviders() + + if (providers != null && providers.isNotEmpty()) { + availableProviders = providers + callback(true) + } else { + LOG.error("No SSO providers available") + callback(false) + } + } catch (e: Exception) { + LOG.error("Error fetching SSO providers", e) + callback(false) + } + }.start() + } + + /** + * Generate SSO URL with PKCE for Custom Tabs + * Returns the URL and also stores the verifier for later + */ + fun generateSsoUrl(idpSlug: String): String { + val codeVerifier = pkceHelper.generateCodeVerifier() + val codeChallenge = pkceHelper.generateCodeChallenge(codeVerifier) + + LOG.debug("Initiating PKCE with verifier=$codeVerifier and challenge=$codeChallenge") + + // Store verifier in shared preferences for the callback activity + getApplication().getSharedPreferences("endurain_oauth_temp", Context.MODE_PRIVATE) + .edit { + putString("code_verifier", codeVerifier) + } + + // Build OAuth URL with custom redirect URI for deep linking + return "$server/api/v1/public/idp/login/$idpSlug".toUri() + .buildUpon() + .appendQueryParameter("code_challenge", codeChallenge) + .appendQueryParameter("code_challenge_method", "S256") + .appendQueryParameter("redirect", BuildConfig.APPLICATION_ID + "://endurain/oauth/callback") + .build() + .toString() + } + + /** + * Exchange OAuth session for tokens + */ + fun exchangeSsoSession(sessionId: String, callback: (Boolean) -> Unit) { + Thread { + try { + // Retrieve the stored code verifier + val prefs = getApplication() + .getSharedPreferences("endurain_oauth_temp", Context.MODE_PRIVATE) + + val verifier = prefs.getString("code_verifier", null) + if (verifier == null) { + LOG.error("No pending code verifier") + callback(false) + return@Thread + } + + // Clear the verifier + prefs.edit { remove("code_verifier") } + + if (!::apiClient.isInitialized) { + apiClient = EndurainApiClient(server, tokenManager) + } + + val response = apiClient.exchangeOAuthSession(sessionId, verifier) + + if (response?.access_token != null) { + LOG.info("SSO login successful") + tokenManager.saveTokens( + response.access_token, + response.refresh_token!!, + response.expires_in!!, + response.refresh_token_expires_in!! + ) + callback(true) + } else { + LOG.error("SSO login failed") + callback(false) + } + } catch (e: Exception) { + LOG.error("SSO session exchange error", e) + callback(false) + } + }.start() + } + /** * Perform local username/password login */ @@ -133,7 +234,8 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic tokenManager.saveTokens( response.access_token, response.refresh_token!!, - response.expires_in!! + response.expires_in!!, + response.refresh_token_expires_in!! ) callback(true) } @@ -164,12 +266,15 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic val response = apiClient.verifyMfa(username, mfaCode) + LOG.debug("MFA verification response: {}", response) + if (response?.access_token != null) { LOG.info("MFA verification successful") tokenManager.saveTokens( response.access_token, response.refresh_token!!, - response.expires_in!! + response.expires_in!!, + response.refresh_token_expires_in!! ) pendingMfaUsername = null callback(true) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSsoProviderDialog.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSsoProviderDialog.kt new file mode 100644 index 0000000000..2d81477e07 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/endurain/EndurainSsoProviderDialog.kt @@ -0,0 +1,41 @@ +/* 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.app.AlertDialog +import android.content.Context +import nodomain.freeyourgadget.gadgetbridge.R + +class EndurainSsoProviderDialog( + private val context: Context, + private val providers: List, + private val onProviderSelected: (IdentityProvider) -> Unit +) { + + fun show() { + val providerNames = providers.map { it.name }.toTypedArray() + + AlertDialog.Builder(context) + .setTitle(context.getString(R.string.endurain_select_identity_provider)) + .setItems(providerNames) { dialog, which -> + onProviderSelected(providers[which]) + dialog.dismiss() + } + .setNegativeButton(context.getString(R.string.cancel), null) + .show() + } +} \ No newline at end of file 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 308500d179..70e404b01f 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 @@ -37,15 +37,15 @@ class EndurainTokenManager(context: Context) { EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) - fun saveTokens(accessToken: String, refreshToken: String, accessTokenExpiresAt: Int) { + fun saveTokens(accessToken: String, refreshToken: String, accessTokenExpiry: Int, refreshTokenExpiry: Int) { + val accessTokenExpiryTs = if (accessTokenExpiry < 1000000000) ((System.currentTimeMillis() / 1000) + accessTokenExpiry).toInt() else accessTokenExpiry + val refreshTokenExpiryTs = if (refreshTokenExpiry < 1000000000) ((System.currentTimeMillis() / 1000) + refreshTokenExpiry).toInt() else refreshTokenExpiry sharedPreferences.edit { putString("access_token", accessToken) .putString("refresh_token", refreshToken) - .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 + .putInt("access_token_expires_at", accessTokenExpiryTs) + .putInt("refresh_token_expires_at", refreshTokenExpiryTs + ) } } @@ -87,7 +87,8 @@ class EndurainTokenManager(context: Context) { saveTokens( response.access_token, response.refresh_token!!, - response.expires_in!! + response.expires_in!!, + response.refresh_token_expires_in!! ) callback(true) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 589f908d6b..81fa53dd5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4938,4 +4938,26 @@ Audio card AUX Prompt tone + Required + SSO login failed + No SSO providers available + Failed to open browser: %1$s + SSO login successful + Invalid server URL + Failed to connect to server + Verify MFA + Enter your MFA code + Login successful + Login failed + MFA verification successful + Invalid MFA code + MFA verification failed + Select Identity Provider + Invalid OAuth callback + Completing login… + Not logged in, integration is disabled + Logged in to %1$s\nRefresh token expires: %2$s + \nServer version: %1$s + Logout failed + Logged out successfully diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b43223714d..7875e3a4f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ searchpreference = "2.7.3" slf4j = "2.0.18" solarpositioning = "0.1.10" security-crypto = "1.1.0" +browser = "1.10.0" [libraries] ahocorasick = { group = "org.ahocorasick", name = "ahocorasick", version.ref = "ahocorasick" } @@ -114,6 +115,7 @@ searchpreference = { group = "com.github.ByteHamster", name = "SearchPreference" slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } solarpositioning = { group = "net.e175.klaus", name = "solarpositioning", version.ref = "solarpositioning" } androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security-crypto" } +androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" } [bundles] android-emojify = [