Loading app/src/main/kotlin/foundation/e/accountmanager/token/MurenaTokenManager.kt 0 → 100644 +230 −0 Original line number 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.token import android.accounts.AccountManager import android.annotation.SuppressLint import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.network.OAuthModule import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Credentials import at.bitfire.davdroid.sync.account.setAndVerifyUserData import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.logging.Level import java.util.logging.Logger import kotlin.time.Duration.Companion.minutes object MurenaTokenManager { const val ACTION_REFRESH = "${BuildConfig.APPLICATION_ID}.action.REFRESH_TOKEN" private const val PENDING_INTENT_REQUEST_CODE = 1001 val earlyTriggerTime = 2.minutes.inWholeMilliseconds val logger: Logger = Logger.getLogger(this.javaClass.name) // Returns a PendingIntent for scheduling or triggering token refresh. fun getPendingIntent(context: Context, noCreate: Boolean = false): PendingIntent? { val flags = if (noCreate) { PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } val intent = Intent(context, TokenRefreshReceiver::class.java).apply { action = ACTION_REFRESH } return PendingIntent.getBroadcast(context, PENDING_INTENT_REQUEST_CODE, intent, flags) } // Cancels any scheduled refresh alarm for the active murena account. fun cancelTokenRefreshAlarm(context: Context) { val alarmManager = context.getSystemService(AlarmManager::class.java) val pendingIntent = getPendingIntent(context, noCreate = true) if (pendingIntent != null) { alarmManager?.cancel(pendingIntent) pendingIntent.cancel() logger.info("Token refresh alarm cancelled") } else { logger.info("No existing token refresh alarm to cancel") } } // Schedules a refresh before the token expires, or refreshes immediately if already expired. fun handleTokenRefresh(context: Context, onComplete: ((AuthState?) -> Unit)? = null) { val credentials = getOrSaveCredentials(context) ?: run { logger.warning("No account credentials found, cannot schedule refresh.") return } val authState = credentials.authState ?: run { logger.warning("Missing AuthState, cannot schedule refresh.") return } val expiration = authState.accessTokenExpirationTime ?: run { logger.warning("Missing token expiration, forcing immediate refresh.") refreshAuthToken(context, onComplete) return } if (SystemUtils.isNetworkAvailable(context)) { // Stop token service if its running val intent = Intent(TokenUpdaterService.ACTION_STOP_TOKEN_SERVICE).apply { setPackage(context.packageName) } context.sendBroadcast(intent) // Request at least 2 minutes early. val refreshAt = expiration.minus(earlyTriggerTime) if (refreshAt <= System.currentTimeMillis()) { logger.info("Token expired or near expiry, refreshing immediately.") refreshAuthToken(context, onComplete) return } else { setTokenRefreshAlarm(context, refreshAt) } } else { logger.warning("No internet connection, starting service") val intent = Intent(context, TokenUpdaterService::class.java) context.startService(intent) } onComplete?.invoke(authState) } // Schedules an exact alarm to refresh the auth token at the specified time. @SuppressLint("ScheduleExactAlarm") private fun setTokenRefreshAlarm(context: Context, timeInMillis: Long) { val alarmManager = context.getSystemService(AlarmManager::class.java) val pendingIntent = getPendingIntent(context) if (alarmManager == null || pendingIntent == null) { logger.warning("could not schedule token refresh alarm.") return } alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent) logger.info("Next token refresh scheduled at ${timeInMillis.asDateString()}") } // Refreshes the authentication token and updates stored credentials if successful. private fun refreshAuthToken(context: Context, onComplete: ((AuthState?) -> Unit)? = null) { try { val authService = OAuthModule.authorizationService(context) val credentials = getOrSaveCredentials(context) ?: run { logger.warning("No account credentials found during token refresh.") return } val authState = credentials.authState ?: run { logger.warning("Missing AuthState during token refresh.") return } if (isInvalidGrant(authState.authorizationException)) { logger.warning("Invalid grant detected, user re-auth required.") cancelTokenRefreshAlarm(context) return } val tokenRequest = authState.createTokenRefreshRequest() authService.performTokenRequest(tokenRequest) { response, exception -> when { response != null && exception == null -> { authState.update(response, null) getOrSaveCredentials(context, credentials.copy(authState = authState)) logger.info("Token refreshed for ${credentials.username}") onComplete?.invoke(authState) } isInvalidGrant(exception) -> { logger.log(Level.SEVERE, "Invalid grant: refresh cancelled, User must re-authenticate.", exception) cancelTokenRefreshAlarm(context) } else -> { logger.log(Level.SEVERE, "Token refresh failed: unknown error, retrying in 5 minutes.") setTokenRefreshAlarm(context, System.currentTimeMillis() + 5.minutes.inWholeMilliseconds) } } } } catch (e: Exception) { logger.log(Level.SEVERE, "Token refresh failed due to unexpected exception.", e) } finally { onComplete?.invoke(null) } } // Checks whether the given AuthorizationException indicates an invalid grant (requires re-login). private fun isInvalidGrant(ex: AuthorizationException?): Boolean { val invalidGrant = AuthorizationException.TokenRequestErrors.INVALID_GRANT return ex?.code == invalidGrant.code && ex.error == invalidGrant.error } // Retrieves the Murena account settings for the currently active account, if available. // We only allow one murena account. fun getOrSaveCredentials(context: Context, newCredentials: Credentials? = null): Credentials? { val accountType = context.getString(R.string.eelo_account_type) val account = AccountManager.get(context) .getAccountsByType(accountType) .firstOrNull() ?: return null.also { logger.info("No Murena account found.") } // Save new credentials if provided newCredentials?.authState?.let { authState -> AccountManager.get(context).setAndVerifyUserData( account, AccountSettings.KEY_AUTH_STATE, authState.jsonSerializeString() ) logger.info("Saved new credentials for account: ${account.name}") AccountHelper.notifyAccountAdded(context, account.name) } // Return current credentials val authState = AccountManager.get(context) .getUserData(account, AccountSettings.KEY_AUTH_STATE) ?.let { AuthState.jsonDeserialize(it) } return Credentials( username = account.name, password = null, authState = authState, certificateAlias = null ) } private fun Long.asDateString(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(this)) } app/src/main/kotlin/foundation/e/accountmanager/token/TokenRefreshReceiver.kt 0 → 100644 +38 −0 Original line number 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.token import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import java.util.logging.Logger class TokenRefreshReceiver : BroadcastReceiver() { val logger: Logger = Logger.getLogger(this.javaClass.name) override fun onReceive(context: Context, intent: Intent) { when (intent.action) { Intent.ACTION_BOOT_COMPLETED, MurenaTokenManager.ACTION_REFRESH -> { MurenaTokenManager.handleTokenRefresh(context.applicationContext) } else -> logger.warning("Received unknown action: ${intent.action}") } } } app/src/main/kotlin/foundation/e/accountmanager/token/TokenUpdaterService.kt 0 → 100644 +102 −0 Original line number 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.token import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest import android.os.Handler import android.os.IBinder import android.os.Looper import androidx.core.content.ContextCompat import at.bitfire.davdroid.BuildConfig import java.util.logging.Logger import kotlin.time.Duration.Companion.seconds class TokenUpdaterService : Service() { val logger: Logger = Logger.getLogger(this.javaClass.name) companion object { const val ACTION_STOP_TOKEN_SERVICE = "${BuildConfig.APPLICATION_ID}.action.STOP_TOKEN_SERVICE" } private val context by lazy { applicationContext } private val connectivityManager by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } private val refreshDelay = 10.seconds.inWholeMilliseconds private val handler = Handler(Looper.getMainLooper()) private val stopReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_STOP_TOKEN_SERVICE -> stopSelf() else -> logger.warning("Received unknown action: ${intent.action}") } } } private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { logger.info("Network became available.") // Send broadcast 10 seconds delayed, let's wait till internet is stable. handler.postDelayed({ val intent = Intent(context.applicationContext, TokenRefreshReceiver::class.java).apply { action = MurenaTokenManager.ACTION_REFRESH } sendBroadcast(intent) }, refreshDelay) } override fun onLost(network: Network) { logger.warning("Network lost, waiting to reconnect.") } } override fun onCreate() { super.onCreate() ContextCompat.registerReceiver(this, stopReceiver, IntentFilter(ACTION_STOP_TOKEN_SERVICE), ContextCompat.RECEIVER_NOT_EXPORTED ) connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), networkCallback) } override fun onDestroy() { super.onDestroy() handler.removeCallbacksAndMessages(null) try { connectivityManager.unregisterNetworkCallback(networkCallback) unregisterReceiver(stopReceiver) } catch (e: Exception) { logger.warning("Error during service cleanup: ${e.message}") } logger.info("Service stopped and cleaned up.") } override fun onBind(intent: Intent?): IBinder? = null } app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt +3 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.settings.AccountSettings import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.sync.SyncBroadcastReceiver import foundation.e.accountmanager.token.MurenaTokenManager import java.util.concurrent.TimeUnit object AccountHelper { Loading Loading @@ -79,6 +80,7 @@ object AccountHelper { intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, AccountTypes.Murena.accountType) context.sendBroadcast(intent, ACCOUNT_EVENTS_PERMISSION) MurenaTokenManager.handleTokenRefresh(context.applicationContext) } fun notifyAccountRemoved(context: Context, intent: Intent) { Loading @@ -86,6 +88,7 @@ object AccountHelper { intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) intent.putExtras(intent) context.sendBroadcast(intent, ACCOUNT_EVENTS_PERMISSION) MurenaTokenManager.cancelTokenRefreshAlarm(context.applicationContext) } fun scheduleSyncWithDelay(context: Context) { Loading app/src/ose/AndroidManifest.xml +18 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,24 @@ </intent-filter> </receiver> <receiver android:name="foundation.e.accountmanager.token.TokenRefreshReceiver" android:exported="true" android:permission="${applicationId}.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="${applicationId}.action.REFRESH_TOKEN" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <service android:name="foundation.e.accountmanager.token.TokenUpdaterService" android:foregroundServiceType="specialUse"> <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="updater" /> </service> <service android:name=".sync.adapter.MurenaCalendarsSyncAdapterService" android:exported="true" Loading Loading
app/src/main/kotlin/foundation/e/accountmanager/token/MurenaTokenManager.kt 0 → 100644 +230 −0 Original line number 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.token import android.accounts.AccountManager import android.annotation.SuppressLint import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.network.OAuthModule import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Credentials import at.bitfire.davdroid.sync.account.setAndVerifyUserData import foundation.e.accountmanager.utils.AccountHelper import foundation.e.accountmanager.utils.SystemUtils import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.logging.Level import java.util.logging.Logger import kotlin.time.Duration.Companion.minutes object MurenaTokenManager { const val ACTION_REFRESH = "${BuildConfig.APPLICATION_ID}.action.REFRESH_TOKEN" private const val PENDING_INTENT_REQUEST_CODE = 1001 val earlyTriggerTime = 2.minutes.inWholeMilliseconds val logger: Logger = Logger.getLogger(this.javaClass.name) // Returns a PendingIntent for scheduling or triggering token refresh. fun getPendingIntent(context: Context, noCreate: Boolean = false): PendingIntent? { val flags = if (noCreate) { PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } val intent = Intent(context, TokenRefreshReceiver::class.java).apply { action = ACTION_REFRESH } return PendingIntent.getBroadcast(context, PENDING_INTENT_REQUEST_CODE, intent, flags) } // Cancels any scheduled refresh alarm for the active murena account. fun cancelTokenRefreshAlarm(context: Context) { val alarmManager = context.getSystemService(AlarmManager::class.java) val pendingIntent = getPendingIntent(context, noCreate = true) if (pendingIntent != null) { alarmManager?.cancel(pendingIntent) pendingIntent.cancel() logger.info("Token refresh alarm cancelled") } else { logger.info("No existing token refresh alarm to cancel") } } // Schedules a refresh before the token expires, or refreshes immediately if already expired. fun handleTokenRefresh(context: Context, onComplete: ((AuthState?) -> Unit)? = null) { val credentials = getOrSaveCredentials(context) ?: run { logger.warning("No account credentials found, cannot schedule refresh.") return } val authState = credentials.authState ?: run { logger.warning("Missing AuthState, cannot schedule refresh.") return } val expiration = authState.accessTokenExpirationTime ?: run { logger.warning("Missing token expiration, forcing immediate refresh.") refreshAuthToken(context, onComplete) return } if (SystemUtils.isNetworkAvailable(context)) { // Stop token service if its running val intent = Intent(TokenUpdaterService.ACTION_STOP_TOKEN_SERVICE).apply { setPackage(context.packageName) } context.sendBroadcast(intent) // Request at least 2 minutes early. val refreshAt = expiration.minus(earlyTriggerTime) if (refreshAt <= System.currentTimeMillis()) { logger.info("Token expired or near expiry, refreshing immediately.") refreshAuthToken(context, onComplete) return } else { setTokenRefreshAlarm(context, refreshAt) } } else { logger.warning("No internet connection, starting service") val intent = Intent(context, TokenUpdaterService::class.java) context.startService(intent) } onComplete?.invoke(authState) } // Schedules an exact alarm to refresh the auth token at the specified time. @SuppressLint("ScheduleExactAlarm") private fun setTokenRefreshAlarm(context: Context, timeInMillis: Long) { val alarmManager = context.getSystemService(AlarmManager::class.java) val pendingIntent = getPendingIntent(context) if (alarmManager == null || pendingIntent == null) { logger.warning("could not schedule token refresh alarm.") return } alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent) logger.info("Next token refresh scheduled at ${timeInMillis.asDateString()}") } // Refreshes the authentication token and updates stored credentials if successful. private fun refreshAuthToken(context: Context, onComplete: ((AuthState?) -> Unit)? = null) { try { val authService = OAuthModule.authorizationService(context) val credentials = getOrSaveCredentials(context) ?: run { logger.warning("No account credentials found during token refresh.") return } val authState = credentials.authState ?: run { logger.warning("Missing AuthState during token refresh.") return } if (isInvalidGrant(authState.authorizationException)) { logger.warning("Invalid grant detected, user re-auth required.") cancelTokenRefreshAlarm(context) return } val tokenRequest = authState.createTokenRefreshRequest() authService.performTokenRequest(tokenRequest) { response, exception -> when { response != null && exception == null -> { authState.update(response, null) getOrSaveCredentials(context, credentials.copy(authState = authState)) logger.info("Token refreshed for ${credentials.username}") onComplete?.invoke(authState) } isInvalidGrant(exception) -> { logger.log(Level.SEVERE, "Invalid grant: refresh cancelled, User must re-authenticate.", exception) cancelTokenRefreshAlarm(context) } else -> { logger.log(Level.SEVERE, "Token refresh failed: unknown error, retrying in 5 minutes.") setTokenRefreshAlarm(context, System.currentTimeMillis() + 5.minutes.inWholeMilliseconds) } } } } catch (e: Exception) { logger.log(Level.SEVERE, "Token refresh failed due to unexpected exception.", e) } finally { onComplete?.invoke(null) } } // Checks whether the given AuthorizationException indicates an invalid grant (requires re-login). private fun isInvalidGrant(ex: AuthorizationException?): Boolean { val invalidGrant = AuthorizationException.TokenRequestErrors.INVALID_GRANT return ex?.code == invalidGrant.code && ex.error == invalidGrant.error } // Retrieves the Murena account settings for the currently active account, if available. // We only allow one murena account. fun getOrSaveCredentials(context: Context, newCredentials: Credentials? = null): Credentials? { val accountType = context.getString(R.string.eelo_account_type) val account = AccountManager.get(context) .getAccountsByType(accountType) .firstOrNull() ?: return null.also { logger.info("No Murena account found.") } // Save new credentials if provided newCredentials?.authState?.let { authState -> AccountManager.get(context).setAndVerifyUserData( account, AccountSettings.KEY_AUTH_STATE, authState.jsonSerializeString() ) logger.info("Saved new credentials for account: ${account.name}") AccountHelper.notifyAccountAdded(context, account.name) } // Return current credentials val authState = AccountManager.get(context) .getUserData(account, AccountSettings.KEY_AUTH_STATE) ?.let { AuthState.jsonDeserialize(it) } return Credentials( username = account.name, password = null, authState = authState, certificateAlias = null ) } private fun Long.asDateString(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(this)) }
app/src/main/kotlin/foundation/e/accountmanager/token/TokenRefreshReceiver.kt 0 → 100644 +38 −0 Original line number 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.token import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import java.util.logging.Logger class TokenRefreshReceiver : BroadcastReceiver() { val logger: Logger = Logger.getLogger(this.javaClass.name) override fun onReceive(context: Context, intent: Intent) { when (intent.action) { Intent.ACTION_BOOT_COMPLETED, MurenaTokenManager.ACTION_REFRESH -> { MurenaTokenManager.handleTokenRefresh(context.applicationContext) } else -> logger.warning("Received unknown action: ${intent.action}") } } }
app/src/main/kotlin/foundation/e/accountmanager/token/TokenUpdaterService.kt 0 → 100644 +102 −0 Original line number 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.token import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest import android.os.Handler import android.os.IBinder import android.os.Looper import androidx.core.content.ContextCompat import at.bitfire.davdroid.BuildConfig import java.util.logging.Logger import kotlin.time.Duration.Companion.seconds class TokenUpdaterService : Service() { val logger: Logger = Logger.getLogger(this.javaClass.name) companion object { const val ACTION_STOP_TOKEN_SERVICE = "${BuildConfig.APPLICATION_ID}.action.STOP_TOKEN_SERVICE" } private val context by lazy { applicationContext } private val connectivityManager by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } private val refreshDelay = 10.seconds.inWholeMilliseconds private val handler = Handler(Looper.getMainLooper()) private val stopReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_STOP_TOKEN_SERVICE -> stopSelf() else -> logger.warning("Received unknown action: ${intent.action}") } } } private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { logger.info("Network became available.") // Send broadcast 10 seconds delayed, let's wait till internet is stable. handler.postDelayed({ val intent = Intent(context.applicationContext, TokenRefreshReceiver::class.java).apply { action = MurenaTokenManager.ACTION_REFRESH } sendBroadcast(intent) }, refreshDelay) } override fun onLost(network: Network) { logger.warning("Network lost, waiting to reconnect.") } } override fun onCreate() { super.onCreate() ContextCompat.registerReceiver(this, stopReceiver, IntentFilter(ACTION_STOP_TOKEN_SERVICE), ContextCompat.RECEIVER_NOT_EXPORTED ) connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), networkCallback) } override fun onDestroy() { super.onDestroy() handler.removeCallbacksAndMessages(null) try { connectivityManager.unregisterNetworkCallback(networkCallback) unregisterReceiver(stopReceiver) } catch (e: Exception) { logger.warning("Error during service cleanup: ${e.message}") } logger.info("Service stopped and cleaned up.") } override fun onBind(intent: Intent?): IBinder? = null }
app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt +3 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.settings.AccountSettings import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.sync.SyncBroadcastReceiver import foundation.e.accountmanager.token.MurenaTokenManager import java.util.concurrent.TimeUnit object AccountHelper { Loading Loading @@ -79,6 +80,7 @@ object AccountHelper { intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, AccountTypes.Murena.accountType) context.sendBroadcast(intent, ACCOUNT_EVENTS_PERMISSION) MurenaTokenManager.handleTokenRefresh(context.applicationContext) } fun notifyAccountRemoved(context: Context, intent: Intent) { Loading @@ -86,6 +88,7 @@ object AccountHelper { intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) intent.putExtras(intent) context.sendBroadcast(intent, ACCOUNT_EVENTS_PERMISSION) MurenaTokenManager.cancelTokenRefreshAlarm(context.applicationContext) } fun scheduleSyncWithDelay(context: Context) { Loading
app/src/ose/AndroidManifest.xml +18 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,24 @@ </intent-filter> </receiver> <receiver android:name="foundation.e.accountmanager.token.TokenRefreshReceiver" android:exported="true" android:permission="${applicationId}.permission.ACCOUNT_EVENTS"> <intent-filter> <action android:name="${applicationId}.action.REFRESH_TOKEN" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <service android:name="foundation.e.accountmanager.token.TokenUpdaterService" android:foregroundServiceType="specialUse"> <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="updater" /> </service> <service android:name=".sync.adapter.MurenaCalendarsSyncAdapterService" android:exported="true" Loading