Endurain: Support SSO when logging into Endurain

This commit is contained in:
Arjan Schrijver
2026-06-19 23:31:23 +02:00
parent 67ee952634
commit 8b9b64b513
11 changed files with 455 additions and 34 deletions
+1
View File
@@ -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
+17
View File
@@ -1124,6 +1124,23 @@
</intent-filter>
</activity>
<activity
android:name=".activities.endurain.EndurainOAuthCallbackActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Deep link scheme for OAuth callback -->
<data
android:scheme="${appAuthRedirectScheme}"
android:host="endurain"
android:pathPrefix="/oauth/callback" />
</intent-filter>
</activity>
<!-- For supported versions through Android 13, create an activity to show the rationale
of Health Connect permissions once users click the privacy policy link. -->
<activity
@@ -37,6 +37,7 @@ data class LoginResponse(
val access_token: String? = null,
val refresh_token: String? = null,
val expires_in: Int? = null,
val refresh_token_expires_in: Int? = null,
val token_type: String? = null,
val mfa_required: Boolean? = null,
val username: String? = null,
@@ -52,6 +53,12 @@ data class TokenExchangeRequest(
val code_verifier: String
)
data class IdentityProvider(
val id: String,
val name: String,
val slug: String
)
class EndurainApiClient(
private val baseUrl: String,
private val tokenManager: EndurainTokenManager
@@ -200,6 +207,28 @@ class EndurainApiClient(
}
}
/**
* Get list of available identity providers
*/
fun getIdentityProviders(): List<IdentityProvider>? {
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<List<IdentityProvider>>() {}.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 {
@@ -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 <https://www.gnu.org/licenses/>. */
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()
}
}
@@ -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
}
@@ -16,11 +16,17 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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)
}
}
}
@@ -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<IdentityProvider> = 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<GBApplication>().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<GBApplication>()
.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)
@@ -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 <https://www.gnu.org/licenses/>. */
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<IdentityProvider>,
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()
}
}
@@ -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)
}
+22
View File
@@ -4938,4 +4938,26 @@
<string name="media_source_audio_card">Audio card</string>
<string name="media_source_aux">AUX</string>
<string name="sinilink_prompt_tone">Prompt tone</string>
<string name="required">Required</string>
<string name="endurain_sso_login_failed">SSO login failed</string>
<string name="endurain_no_sso_providers_available">No SSO providers available</string>
<string name="endurain_failed_to_open_browser">Failed to open browser: %1$s</string>
<string name="endurain_sso_login_successful">SSO login successful</string>
<string name="endurain_invalid_server_url">Invalid server URL</string>
<string name="endurain_failed_to_connect_to_server">Failed to connect to server</string>
<string name="endurain_verify_mfa">Verify MFA</string>
<string name="endurain_enter_your_mfa_code">Enter your MFA code</string>
<string name="endurain_login_successful">Login successful</string>
<string name="endurain_login_failed">Login failed</string>
<string name="endurain_mfa_verification_successful">MFA verification successful</string>
<string name="endurain_invalid_mfa_code">Invalid MFA code</string>
<string name="endurain_mfa_verification_failed">MFA verification failed</string>
<string name="endurain_select_identity_provider">Select Identity Provider</string>
<string name="endurain_invalid_oauth_callback">Invalid OAuth callback</string>
<string name="endurain_oauth_completing_login">Completing login…</string>
<string name="endurain_not_logged_in_integration_disabled">Not logged in, integration is disabled</string>
<string name="endurain_logged_in_refresh_token">Logged in to %1$s\nRefresh token expires: %2$s</string>
<string name="endurain_server_version">\nServer version: %1$s</string>
<string name="endurain_logout_failed">Logout failed</string>
<string name="endurain_logged_out_successfully">Logged out successfully</string>
</resources>