Loading app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +12 −4 Original line number Original line Diff line number Diff line Loading @@ -35,10 +35,11 @@ import at.bitfire.davdroid.ui.account.AccountSettingsActivity import dagger.assisted.Assisted import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.ui.setup.ReOAuthActivity import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible import java.io.IOException import java.io.IOException import java.io.InterruptedIOException import java.util.logging.Level import java.util.logging.Level import java.util.logging.Logger import java.util.logging.Logger import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException Loading Loading @@ -188,10 +189,17 @@ class RefreshCollectionsWorker @AssistedInject constructor( } catch (e: UnauthorizedException) { } catch (e: UnauthorizedException) { logger.log(Level.SEVERE, "Not authorized (anymore)", e) logger.log(Level.SEVERE, "Not authorized (anymore)", e) // notify that we need to re-authenticate in the account settings // notify that we need to re-authenticate in the account settings val settingsIntent = Intent(applicationContext, AccountSettingsActivity::class.java) val isOidcAccount = AccountHelper.isOidcAccount(applicationContext, account) .putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) val (settingsIntent, notificationMessage) = if (isOidcAccount) { Intent(applicationContext, ReOAuthActivity::class.java) to applicationContext.getString(R.string.sync_error_authentication_oauth) } else { Intent(applicationContext, AccountSettingsActivity::class.java) to applicationContext.getString(R.string.sync_error_authentication_failed) } settingsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) notifyRefreshError( notifyRefreshError( applicationContext.getString(R.string.sync_error_authentication_failed), notificationMessage, settingsIntent settingsIntent ) ) return Result.failure() return Result.failure() Loading app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +6 −1 Original line number Original line Diff line number Diff line Loading @@ -43,6 +43,7 @@ import at.bitfire.davdroid.resource.SyncState import at.bitfire.davdroid.sync.account.InvalidAccountException import at.bitfire.davdroid.sync.account.InvalidAccountException import at.bitfire.synctools.storage.LocalStorageException import at.bitfire.synctools.storage.LocalStorageException import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils import foundation.e.accountmanager.utils.SystemUtils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope Loading Loading @@ -779,7 +780,11 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo if (isNetworkAvailable) { if (isNetworkAvailable) { syncResult.numAuthExceptions++ syncResult.numAuthExceptions++ } } message = context.getString(R.string.sync_error_authentication_failed) message = if (AccountHelper.isOidcAccount(context, account)) { context.getString(R.string.sync_error_authentication_oauth) } else { context.getString(R.string.sync_error_authentication_failed) } } } is HttpException, is DavException -> { is HttpException, is DavException -> { Loading app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt +7 −1 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.ui.setup.ReOAuthActivity import foundation.e.accountmanager.utils.AccountHelper import okhttp3.HttpUrl import okhttp3.HttpUrl import java.io.IOException import java.io.IOException import java.util.logging.Level import java.util.logging.Level Loading Loading @@ -116,7 +118,11 @@ class SyncNotificationManager @AssistedInject constructor( ) = notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) { ) = notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) { val contentIntent: Intent val contentIntent: Intent if (e is UnauthorizedException) { if (e is UnauthorizedException) { contentIntent = Intent(context, AccountSettingsActivity::class.java) contentIntent = if (AccountHelper.isOidcAccount(context, account)) { Intent(context, ReOAuthActivity::class.java) } else { Intent(context, AccountSettingsActivity::class.java) } contentIntent.putExtra( contentIntent.putExtra( AccountSettingsActivity.EXTRA_ACCOUNT, AccountSettingsActivity.EXTRA_ACCOUNT, account account Loading app/src/main/kotlin/foundation/e/accountmanager/ui/setup/ReOAuthActivity.kt 0 → 100644 +96 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import android.accounts.Account import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.glance.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import at.bitfire.davdroid.ui.AppTheme import at.bitfire.davdroid.ui.account.AccountSettingsActivity import at.bitfire.davdroid.ui.account.AccountSettingsModel import dagger.hilt.android.AndroidEntryPoint import foundation.e.accountmanager.utils.AccountHelper @AndroidEntryPoint class ReOAuthActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Retrieve the Account from the Intent val account: Account? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(AccountSettingsActivity.EXTRA_ACCOUNT, Account::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra(AccountSettingsActivity.EXTRA_ACCOUNT) } setContent { AppTheme { if (account != null) { OAuthHandlerScreen( account = account, onFinished = { finish() } ) } else { finish() } } } } } @Composable fun OAuthHandlerScreen( account: Account, onFinished: () -> Unit, ) { val context = LocalContext.current val model = hiltViewModel { factory: AccountSettingsModel.Factory -> factory.create(account) } val authRequestContract = rememberLauncherForActivityResult(model.authorizationContract()) { authResponse -> if (authResponse != null) { model.authenticate(authResponse) // Sync after authenticated AccountHelper.scheduleSyncWithDelay(context) } else { model.authCodeFailed() } onFinished() } // Auto-launch immediately, no UI shown LaunchedEffect(Unit) { val request = model.newAuthorizationRequest() if (request != null) { authRequestContract.launch(request) } else { onFinished() } } } app/src/main/res/values/e_strings.xml +2 −0 Original line number Original line Diff line number Diff line Loading @@ -34,4 +34,6 @@ <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="legacy_murena_login">Legacy Murena.io</string> <string name="legacy_murena_login">Legacy Murena.io</string> <string name="sync_error_authentication_oauth">Authentication issue. Tap to sign in again</string> </resources> </resources> Loading
app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +12 −4 Original line number Original line Diff line number Diff line Loading @@ -35,10 +35,11 @@ import at.bitfire.davdroid.ui.account.AccountSettingsActivity import dagger.assisted.Assisted import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.ui.setup.ReOAuthActivity import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible import java.io.IOException import java.io.IOException import java.io.InterruptedIOException import java.util.logging.Level import java.util.logging.Level import java.util.logging.Logger import java.util.logging.Logger import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException Loading Loading @@ -188,10 +189,17 @@ class RefreshCollectionsWorker @AssistedInject constructor( } catch (e: UnauthorizedException) { } catch (e: UnauthorizedException) { logger.log(Level.SEVERE, "Not authorized (anymore)", e) logger.log(Level.SEVERE, "Not authorized (anymore)", e) // notify that we need to re-authenticate in the account settings // notify that we need to re-authenticate in the account settings val settingsIntent = Intent(applicationContext, AccountSettingsActivity::class.java) val isOidcAccount = AccountHelper.isOidcAccount(applicationContext, account) .putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) val (settingsIntent, notificationMessage) = if (isOidcAccount) { Intent(applicationContext, ReOAuthActivity::class.java) to applicationContext.getString(R.string.sync_error_authentication_oauth) } else { Intent(applicationContext, AccountSettingsActivity::class.java) to applicationContext.getString(R.string.sync_error_authentication_failed) } settingsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) notifyRefreshError( notifyRefreshError( applicationContext.getString(R.string.sync_error_authentication_failed), notificationMessage, settingsIntent settingsIntent ) ) return Result.failure() return Result.failure() Loading
app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +6 −1 Original line number Original line Diff line number Diff line Loading @@ -43,6 +43,7 @@ import at.bitfire.davdroid.resource.SyncState import at.bitfire.davdroid.sync.account.InvalidAccountException import at.bitfire.davdroid.sync.account.InvalidAccountException import at.bitfire.synctools.storage.LocalStorageException import at.bitfire.synctools.storage.LocalStorageException import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils import foundation.e.accountmanager.utils.SystemUtils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope Loading Loading @@ -779,7 +780,11 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo if (isNetworkAvailable) { if (isNetworkAvailable) { syncResult.numAuthExceptions++ syncResult.numAuthExceptions++ } } message = context.getString(R.string.sync_error_authentication_failed) message = if (AccountHelper.isOidcAccount(context, account)) { context.getString(R.string.sync_error_authentication_oauth) } else { context.getString(R.string.sync_error_authentication_failed) } } } is HttpException, is DavException -> { is HttpException, is DavException -> { Loading
app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt +7 −1 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.ui.setup.ReOAuthActivity import foundation.e.accountmanager.utils.AccountHelper import okhttp3.HttpUrl import okhttp3.HttpUrl import java.io.IOException import java.io.IOException import java.util.logging.Level import java.util.logging.Level Loading Loading @@ -116,7 +118,11 @@ class SyncNotificationManager @AssistedInject constructor( ) = notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) { ) = notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) { val contentIntent: Intent val contentIntent: Intent if (e is UnauthorizedException) { if (e is UnauthorizedException) { contentIntent = Intent(context, AccountSettingsActivity::class.java) contentIntent = if (AccountHelper.isOidcAccount(context, account)) { Intent(context, ReOAuthActivity::class.java) } else { Intent(context, AccountSettingsActivity::class.java) } contentIntent.putExtra( contentIntent.putExtra( AccountSettingsActivity.EXTRA_ACCOUNT, AccountSettingsActivity.EXTRA_ACCOUNT, account account Loading
app/src/main/kotlin/foundation/e/accountmanager/ui/setup/ReOAuthActivity.kt 0 → 100644 +96 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import android.accounts.Account import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.glance.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import at.bitfire.davdroid.ui.AppTheme import at.bitfire.davdroid.ui.account.AccountSettingsActivity import at.bitfire.davdroid.ui.account.AccountSettingsModel import dagger.hilt.android.AndroidEntryPoint import foundation.e.accountmanager.utils.AccountHelper @AndroidEntryPoint class ReOAuthActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Retrieve the Account from the Intent val account: Account? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(AccountSettingsActivity.EXTRA_ACCOUNT, Account::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra(AccountSettingsActivity.EXTRA_ACCOUNT) } setContent { AppTheme { if (account != null) { OAuthHandlerScreen( account = account, onFinished = { finish() } ) } else { finish() } } } } } @Composable fun OAuthHandlerScreen( account: Account, onFinished: () -> Unit, ) { val context = LocalContext.current val model = hiltViewModel { factory: AccountSettingsModel.Factory -> factory.create(account) } val authRequestContract = rememberLauncherForActivityResult(model.authorizationContract()) { authResponse -> if (authResponse != null) { model.authenticate(authResponse) // Sync after authenticated AccountHelper.scheduleSyncWithDelay(context) } else { model.authCodeFailed() } onFinished() } // Auto-launch immediately, no UI shown LaunchedEffect(Unit) { val request = model.newAuthorizationRequest() if (request != null) { authRequestContract.launch(request) } else { onFinished() } } }
app/src/main/res/values/e_strings.xml +2 −0 Original line number Original line Diff line number Diff line Loading @@ -34,4 +34,6 @@ <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="legacy_murena_login">Legacy Murena.io</string> <string name="legacy_murena_login">Legacy Murena.io</string> <string name="sync_error_authentication_oauth">Authentication issue. Tap to sign in again</string> </resources> </resources>