Loading app/src/main/AndroidManifest.xml +4 −2 Original line number Diff line number Diff line Loading @@ -129,7 +129,6 @@ android:name=".ui.setup.LoginActivity" android:label="@string/login_title" android:parentActivityName=".ui.AccountsActivity" android:excludeFromRecents="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> Loading Loading @@ -213,6 +212,9 @@ android:exported="true" /> <!-- account type "DAVx⁵" --> <service android:name=".murenasso.MurenaSsoMigrationService" android:exported="false"/> <service android:name=".syncadapter.AccountAuthenticatorService" android:exported="false"> Loading app/src/main/java/com/nextcloud/android/utils/AccountManagerUtils.kt→app/src/main/java/com/nextcloud/android/utils/WebViewUtils.kt +0 −0 File moved. View file app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt 0 → 100644 +42 −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 at.bitfire.davdroid.murenasso import android.content.Context import androidx.core.content.edit object MurenaSsoMigrationPreferences { private const val PREFERENCES_NAME = "murena_sso_migration" private const val KEY_IS_MURENA_SSO_MIGRATION_RUNNING = "is_sso_migration_running" fun isSsoMigrationRunning(context: Context): Boolean { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) return preferences.getBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false) } fun updateSsoMigrationStatus(context: Context, status: MigrationStatus) { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) val isRunning = (status == MigrationStatus.InProgress) preferences.edit { putBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, isRunning) } } enum class MigrationStatus { InProgress, Completed } } app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt 0 → 100644 +119 −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 at.bitfire.davdroid.murenasso import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.app.Service import android.content.Intent import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle import android.os.IBinder import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences.MigrationStatus import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.setup.LoginActivity class MurenaSsoMigrationService : Service() { companion object { const val NOTIFICATION_TAG_MURENA_SSO = "MIGRATE_TO_MURENA_SSO" } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val accountName = intent?.getStringExtra(LoginActivity.EXTRA_USERNAME) if (accountName.isNullOrBlank()) { stopSelf() return START_NOT_STICKY } openMurenaLoginUi(accountName) return START_STICKY } private fun openMurenaLoginUi(accountName: String?) { val accountManager = AccountManager.get(applicationContext) val eAccountType = applicationContext.getString(R.string.eelo_account_type) accountManager.addAccount( eAccountType, null, null, null, null, { future -> val intent = getIntentFromFuture(future) if (intent == null) { stopSelf() // Stops the service and dismisses the notification as migration isn't possible. return@addAccount } setMigrationStatusInProgress() // Calling startActivity() from outside of an Activity context // requires the FLAG_ACTIVITY_NEW_TASK flag intent.apply { putExtra(LoginActivity.EXTRA_USERNAME, accountName) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } ContextCompat.startActivity(applicationContext, intent, null) }, null ) } private fun getIntentFromFuture(future: AccountManagerFuture<Bundle>): Intent? { val bundle = future.result return try { if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { bundle?.getParcelable(AccountManager.KEY_INTENT, Intent::class.java) } else { @Suppress("DEPRECATION") bundle?.getParcelable(AccountManager.KEY_INTENT) } } catch (exception: Exception) { Logger.log.warning("${exception.javaClass}: can't add account: ${exception.message}") null } } private fun setMigrationStatusInProgress() { MurenaSsoMigrationPreferences.updateSsoMigrationStatus( applicationContext, MigrationStatus.InProgress ) } override fun onBind(intent: Intent?): IBinder? { return null } override fun onDestroy() { NotificationManagerCompat.from(applicationContext).cancel( NOTIFICATION_TAG_MURENA_SSO, NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO ) Logger.log.info("Stopping MurenaSsoMigrationService.") super.onDestroy() } } app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +98 −4 Original line number Diff line number Diff line Loading @@ -4,12 +4,31 @@ package at.bitfire.davdroid.receiver import android.Manifest.permission.POST_NOTIFICATIONS import android.accounts.Account import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService.Companion.NOTIFICATION_TAG_MURENA_SSO import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.CHANNEL_GENERAL import at.bitfire.davdroid.ui.NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.setup.LoginActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch /** * There are circumstances when Android drops automatic sync of accounts and resets them Loading @@ -19,15 +38,90 @@ import at.bitfire.davdroid.syncadapter.AccountUtils * is started, it checks (and repairs, if necessary) the sync intervals in [App.onCreate]. */ class BootCompletedReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Logger.log.info("Device has been rebooted; checking sync intervals etc.") // sync intervals are checked in App.onCreate() val pendingResult = goAsync() val job = SupervisorJob() val scope = CoroutineScope(job + Dispatchers.IO) scope.launch { try { initializeSync(context) handleMurenaSsoMigration(context) } finally { pendingResult.finish() job.cancel() } } } private fun initializeSync(context: Context) { AccountUtils.getMainAccounts(context) .forEach { // sync intervals are checked in App.onCreate() val accountSettings = AccountSettings(context, it) accountSettings.initSync() } } private fun handleMurenaSsoMigration( context: Context ) { val account = findAccountForSsoMigration(context) if (account != null) { notifySsoMigration(context, account) } } private fun findAccountForSsoMigration(context: Context): Account? { return AccountUtils.getMainAccounts(context) .firstOrNull { AccountUtils.isMurenaAccount(context, it) && !AccountUtils.isLoggedInWithMurenaSso(context, it) } } private fun notifySsoMigration(context: Context, account: Account) { if (!hasPermission(context)) { Logger.log.warning("Notification permission is not granted, skipping SSO migration.") return } val title = context.getString(R.string.notification_title_murena_sso, account.name) val contentText = context.getString(R.string.notification_text_murena_sso) val intent = createSsoMigrationIntent(context, account.name) val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) .setOngoing(true) .setContentTitle(title) .setContentText(contentText) .setSmallIcon(R.drawable.ic_info) .setContentIntent(intent) .setAutoCancel(false) .build() val notificationManager = NotificationManagerCompat.from(context) notificationManager.notifyIfPossible(NOTIFICATION_TAG_MURENA_SSO, NOTIFY_MIGRATE_TO_MURENA_SSO, notification) } private fun hasPermission(context: Context): Boolean { if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { val permission = POST_NOTIFICATIONS if (context.checkSelfPermission(permission) != PERMISSION_GRANTED) { return false } } return true } private fun createSsoMigrationIntent(context: Context, accountName: String): PendingIntent { val serviceIntent = Intent(context, MurenaSsoMigrationService::class.java).apply { putExtra(LoginActivity.EXTRA_USERNAME, accountName) flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } return PendingIntent.getService(context, 0, serviceIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } } Loading
app/src/main/AndroidManifest.xml +4 −2 Original line number Diff line number Diff line Loading @@ -129,7 +129,6 @@ android:name=".ui.setup.LoginActivity" android:label="@string/login_title" android:parentActivityName=".ui.AccountsActivity" android:excludeFromRecents="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> Loading Loading @@ -213,6 +212,9 @@ android:exported="true" /> <!-- account type "DAVx⁵" --> <service android:name=".murenasso.MurenaSsoMigrationService" android:exported="false"/> <service android:name=".syncadapter.AccountAuthenticatorService" android:exported="false"> Loading
app/src/main/java/com/nextcloud/android/utils/AccountManagerUtils.kt→app/src/main/java/com/nextcloud/android/utils/WebViewUtils.kt +0 −0 File moved. View file
app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt 0 → 100644 +42 −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 at.bitfire.davdroid.murenasso import android.content.Context import androidx.core.content.edit object MurenaSsoMigrationPreferences { private const val PREFERENCES_NAME = "murena_sso_migration" private const val KEY_IS_MURENA_SSO_MIGRATION_RUNNING = "is_sso_migration_running" fun isSsoMigrationRunning(context: Context): Boolean { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) return preferences.getBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false) } fun updateSsoMigrationStatus(context: Context, status: MigrationStatus) { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) val isRunning = (status == MigrationStatus.InProgress) preferences.edit { putBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, isRunning) } } enum class MigrationStatus { InProgress, Completed } }
app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt 0 → 100644 +119 −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 at.bitfire.davdroid.murenasso import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.app.Service import android.content.Intent import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle import android.os.IBinder import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences.MigrationStatus import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.setup.LoginActivity class MurenaSsoMigrationService : Service() { companion object { const val NOTIFICATION_TAG_MURENA_SSO = "MIGRATE_TO_MURENA_SSO" } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val accountName = intent?.getStringExtra(LoginActivity.EXTRA_USERNAME) if (accountName.isNullOrBlank()) { stopSelf() return START_NOT_STICKY } openMurenaLoginUi(accountName) return START_STICKY } private fun openMurenaLoginUi(accountName: String?) { val accountManager = AccountManager.get(applicationContext) val eAccountType = applicationContext.getString(R.string.eelo_account_type) accountManager.addAccount( eAccountType, null, null, null, null, { future -> val intent = getIntentFromFuture(future) if (intent == null) { stopSelf() // Stops the service and dismisses the notification as migration isn't possible. return@addAccount } setMigrationStatusInProgress() // Calling startActivity() from outside of an Activity context // requires the FLAG_ACTIVITY_NEW_TASK flag intent.apply { putExtra(LoginActivity.EXTRA_USERNAME, accountName) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } ContextCompat.startActivity(applicationContext, intent, null) }, null ) } private fun getIntentFromFuture(future: AccountManagerFuture<Bundle>): Intent? { val bundle = future.result return try { if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { bundle?.getParcelable(AccountManager.KEY_INTENT, Intent::class.java) } else { @Suppress("DEPRECATION") bundle?.getParcelable(AccountManager.KEY_INTENT) } } catch (exception: Exception) { Logger.log.warning("${exception.javaClass}: can't add account: ${exception.message}") null } } private fun setMigrationStatusInProgress() { MurenaSsoMigrationPreferences.updateSsoMigrationStatus( applicationContext, MigrationStatus.InProgress ) } override fun onBind(intent: Intent?): IBinder? { return null } override fun onDestroy() { NotificationManagerCompat.from(applicationContext).cancel( NOTIFICATION_TAG_MURENA_SSO, NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO ) Logger.log.info("Stopping MurenaSsoMigrationService.") super.onDestroy() } }
app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +98 −4 Original line number Diff line number Diff line Loading @@ -4,12 +4,31 @@ package at.bitfire.davdroid.receiver import android.Manifest.permission.POST_NOTIFICATIONS import android.accounts.Account import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService.Companion.NOTIFICATION_TAG_MURENA_SSO import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.CHANNEL_GENERAL import at.bitfire.davdroid.ui.NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.setup.LoginActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch /** * There are circumstances when Android drops automatic sync of accounts and resets them Loading @@ -19,15 +38,90 @@ import at.bitfire.davdroid.syncadapter.AccountUtils * is started, it checks (and repairs, if necessary) the sync intervals in [App.onCreate]. */ class BootCompletedReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Logger.log.info("Device has been rebooted; checking sync intervals etc.") // sync intervals are checked in App.onCreate() val pendingResult = goAsync() val job = SupervisorJob() val scope = CoroutineScope(job + Dispatchers.IO) scope.launch { try { initializeSync(context) handleMurenaSsoMigration(context) } finally { pendingResult.finish() job.cancel() } } } private fun initializeSync(context: Context) { AccountUtils.getMainAccounts(context) .forEach { // sync intervals are checked in App.onCreate() val accountSettings = AccountSettings(context, it) accountSettings.initSync() } } private fun handleMurenaSsoMigration( context: Context ) { val account = findAccountForSsoMigration(context) if (account != null) { notifySsoMigration(context, account) } } private fun findAccountForSsoMigration(context: Context): Account? { return AccountUtils.getMainAccounts(context) .firstOrNull { AccountUtils.isMurenaAccount(context, it) && !AccountUtils.isLoggedInWithMurenaSso(context, it) } } private fun notifySsoMigration(context: Context, account: Account) { if (!hasPermission(context)) { Logger.log.warning("Notification permission is not granted, skipping SSO migration.") return } val title = context.getString(R.string.notification_title_murena_sso, account.name) val contentText = context.getString(R.string.notification_text_murena_sso) val intent = createSsoMigrationIntent(context, account.name) val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) .setOngoing(true) .setContentTitle(title) .setContentText(contentText) .setSmallIcon(R.drawable.ic_info) .setContentIntent(intent) .setAutoCancel(false) .build() val notificationManager = NotificationManagerCompat.from(context) notificationManager.notifyIfPossible(NOTIFICATION_TAG_MURENA_SSO, NOTIFY_MIGRATE_TO_MURENA_SSO, notification) } private fun hasPermission(context: Context): Boolean { if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { val permission = POST_NOTIFICATIONS if (context.checkSelfPermission(permission) != PERMISSION_GRANTED) { return false } } return true } private fun createSsoMigrationIntent(context: Context, accountName: String): PendingIntent { val serviceIntent = Intent(context, MurenaSsoMigrationService::class.java).apply { putExtra(LoginActivity.EXTRA_USERNAME, accountName) flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } return PendingIntent.getService(context, 0, serviceIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } }