Loading .gitlab-ci.yml +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ stages: before_script: - echo MURENA_CLIENT_ID=$MURENA_CLIENT_ID >> local.properties - echo MURENA_REDIRECT_URI=$MURENA_REDIRECT_URI >> local.properties - echo MURENA_LOGOUT_REDIRECT_URI=$MURENA_LOGOUT_REDIRECT_URI >> local.properties - echo MURENA_BASE_URL=$MURENA_BASE_URL >> local.properties - echo MURENA_DISCOVERY_END_POINT=$MURENA_DISCOVERY_END_POINT >> local.properties - export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 Loading app/build.gradle.kts +2 −0 Original line number Diff line number Diff line Loading @@ -73,12 +73,14 @@ android { buildConfigField("String", "MURENA_CLIENT_ID", "\"${retrieveKey("MURENA_CLIENT_ID")}\"") buildConfigField("String", "MURENA_REDIRECT_URI", "\"${retrieveKey("MURENA_REDIRECT_URI")}\"") buildConfigField("String", "MURENA_LOGOUT_REDIRECT_URI", "\"${retrieveKey("MURENA_LOGOUT_REDIRECT_URI")}\"") buildConfigField("String", "MURENA_BASE_URL", "\"${retrieveKey("MURENA_BASE_URL")}\"") buildConfigField("String", "MURENA_DISCOVERY_END_POINT", "\"${retrieveKey("MURENA_DISCOVERY_END_POINT")}\"") manifestPlaceholders.putAll( mapOf<String, Any>( "murenaAuthRedirectScheme" to retrieveKey("MURENA_REDIRECT_URI"), "murenaAuthLogoutRedirectScheme" to retrieveKey("MURENA_LOGOUT_REDIRECT_URI") ) ) } Loading app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +2 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow Loading Loading @@ -79,6 +80,7 @@ class AccountRepository @Inject constructor( // create Android account val userData = AccountSettings.initialUserData(credentials) AuthStatePrefUtils.saveAuthState(context, account, credentials?.authState?.jsonSerializeString()) logger.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) if (!SystemAccountUtils.createAccount(context, account, userData, credentials?.password)) Loading app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +2 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import net.openid.appauth.AuthState import java.util.Collections import java.util.logging.Level Loading Loading @@ -132,6 +133,7 @@ class AccountSettings @AssistedInject constructor( fun updateAuthState(authState: AuthState) { accountManager.setAndVerifyUserData(account, KEY_AUTH_STATE, authState.jsonSerializeString()) AuthStatePrefUtils.saveAuthState(context, account, authState.jsonSerializeString()) } /** Loading app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt 0 → 100644 +107 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 eFoundation * * 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.auth import android.accounts.AccountManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.AccountSettings.Companion.KEY_AUTH_STATE import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import foundation.e.accountmanager.ui.setup.MurenaLogoutActivity import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils.sendWithBackgroundLaunchAllowed import java.util.logging.Level import java.util.logging.Logger class AccountReceiver : BroadcastReceiver() { val logger: Logger = Logger.getLogger(this.javaClass.name) override fun onReceive(context: Context?, intent: Intent?) { if (context == null || intent == null) return when (intent.action) { AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION -> handleAccountChange(context) AccountManager.ACTION_ACCOUNT_REMOVED -> handleAccountRemoval(context, intent) else -> logger.log(Level.WARNING, "Unhandled action: ${intent.action}") } } private fun handleAccountChange(context: Context) { val accountManager = AccountManager.get(context) val murenaAccounts = AccountHelper.getAllAccounts(accountManager) .filter { it.type == AccountTypes.Murena.accountType } murenaAccounts.forEach { account -> logger.log(Level.INFO, "Account change detected for ${account.name}") val authState = accountManager.getUserData(account, KEY_AUTH_STATE) AuthStatePrefUtils.saveAuthState(context, account, authState) } } private fun handleAccountRemoval(context: Context, intent: Intent) { val accountType = intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) ?: return val accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) ?: return logger.log(Level.INFO, "Account removed - Type: $accountType, Name: $accountName") if (accountType != AccountTypes.Murena.accountType) { logger.log(Level.INFO, "Unrecognized account type $accountType, ignoring") return } clearOidcSession(context, accountName, accountType) val accountManager = AccountManager.get(context) val addressBooks = accountManager.getAccountsByType(AccountTypes.Murena.addressBookType) addressBooks.forEach { accountManager.removeAccountExplicitly(it) } } private fun clearOidcSession(context: Context, accountName: String, accountType: String) { val authState = AuthStatePrefUtils.loadAuthState(context, accountName, accountType) if (authState == null) { logger.log(Level.SEVERE, "AuthState is null, cannot initiate end session for $accountName and $accountType") return } AuthStatePrefUtils.removeAuthState(context, accountName, accountType) startOidcEndSessionActivity(context, authState, accountType) } private fun startOidcEndSessionActivity(context: Context, authState: String, accountType: String) { val intent = Intent(context, MurenaLogoutActivity::class.java).apply { putExtra(AccountSettings.KEY_AUTH_STATE, authState) putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) pendingIntent.sendWithBackgroundLaunchAllowed() } } Loading
.gitlab-ci.yml +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ stages: before_script: - echo MURENA_CLIENT_ID=$MURENA_CLIENT_ID >> local.properties - echo MURENA_REDIRECT_URI=$MURENA_REDIRECT_URI >> local.properties - echo MURENA_LOGOUT_REDIRECT_URI=$MURENA_LOGOUT_REDIRECT_URI >> local.properties - echo MURENA_BASE_URL=$MURENA_BASE_URL >> local.properties - echo MURENA_DISCOVERY_END_POINT=$MURENA_DISCOVERY_END_POINT >> local.properties - export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 Loading
app/build.gradle.kts +2 −0 Original line number Diff line number Diff line Loading @@ -73,12 +73,14 @@ android { buildConfigField("String", "MURENA_CLIENT_ID", "\"${retrieveKey("MURENA_CLIENT_ID")}\"") buildConfigField("String", "MURENA_REDIRECT_URI", "\"${retrieveKey("MURENA_REDIRECT_URI")}\"") buildConfigField("String", "MURENA_LOGOUT_REDIRECT_URI", "\"${retrieveKey("MURENA_LOGOUT_REDIRECT_URI")}\"") buildConfigField("String", "MURENA_BASE_URL", "\"${retrieveKey("MURENA_BASE_URL")}\"") buildConfigField("String", "MURENA_DISCOVERY_END_POINT", "\"${retrieveKey("MURENA_DISCOVERY_END_POINT")}\"") manifestPlaceholders.putAll( mapOf<String, Any>( "murenaAuthRedirectScheme" to retrieveKey("MURENA_REDIRECT_URI"), "murenaAuthLogoutRedirectScheme" to retrieveKey("MURENA_LOGOUT_REDIRECT_URI") ) ) } Loading
app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +2 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow Loading Loading @@ -79,6 +80,7 @@ class AccountRepository @Inject constructor( // create Android account val userData = AccountSettings.initialUserData(credentials) AuthStatePrefUtils.saveAuthState(context, account, credentials?.authState?.jsonSerializeString()) logger.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) if (!SystemAccountUtils.createAccount(context, account, userData, credentials?.password)) Loading
app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +2 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import net.openid.appauth.AuthState import java.util.Collections import java.util.logging.Level Loading Loading @@ -132,6 +133,7 @@ class AccountSettings @AssistedInject constructor( fun updateAuthState(authState: AuthState) { accountManager.setAndVerifyUserData(account, KEY_AUTH_STATE, authState.jsonSerializeString()) AuthStatePrefUtils.saveAuthState(context, account, authState.jsonSerializeString()) } /** Loading
app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt 0 → 100644 +107 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 eFoundation * * 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.auth import android.accounts.AccountManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.AccountSettings.Companion.KEY_AUTH_STATE import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.pref.AuthStatePrefUtils import foundation.e.accountmanager.ui.setup.MurenaLogoutActivity import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils.sendWithBackgroundLaunchAllowed import java.util.logging.Level import java.util.logging.Logger class AccountReceiver : BroadcastReceiver() { val logger: Logger = Logger.getLogger(this.javaClass.name) override fun onReceive(context: Context?, intent: Intent?) { if (context == null || intent == null) return when (intent.action) { AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION -> handleAccountChange(context) AccountManager.ACTION_ACCOUNT_REMOVED -> handleAccountRemoval(context, intent) else -> logger.log(Level.WARNING, "Unhandled action: ${intent.action}") } } private fun handleAccountChange(context: Context) { val accountManager = AccountManager.get(context) val murenaAccounts = AccountHelper.getAllAccounts(accountManager) .filter { it.type == AccountTypes.Murena.accountType } murenaAccounts.forEach { account -> logger.log(Level.INFO, "Account change detected for ${account.name}") val authState = accountManager.getUserData(account, KEY_AUTH_STATE) AuthStatePrefUtils.saveAuthState(context, account, authState) } } private fun handleAccountRemoval(context: Context, intent: Intent) { val accountType = intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) ?: return val accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) ?: return logger.log(Level.INFO, "Account removed - Type: $accountType, Name: $accountName") if (accountType != AccountTypes.Murena.accountType) { logger.log(Level.INFO, "Unrecognized account type $accountType, ignoring") return } clearOidcSession(context, accountName, accountType) val accountManager = AccountManager.get(context) val addressBooks = accountManager.getAccountsByType(AccountTypes.Murena.addressBookType) addressBooks.forEach { accountManager.removeAccountExplicitly(it) } } private fun clearOidcSession(context: Context, accountName: String, accountType: String) { val authState = AuthStatePrefUtils.loadAuthState(context, accountName, accountType) if (authState == null) { logger.log(Level.SEVERE, "AuthState is null, cannot initiate end session for $accountName and $accountType") return } AuthStatePrefUtils.removeAuthState(context, accountName, accountType) startOidcEndSessionActivity(context, authState, accountType) } private fun startOidcEndSessionActivity(context: Context, authState: String, accountType: String) { val intent = Intent(context, MurenaLogoutActivity::class.java).apply { putExtra(AccountSettings.KEY_AUTH_STATE, authState) putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) pendingIntent.sendWithBackgroundLaunchAllowed() } }