From ff834b6d9f053d0e27f3ad7f2d4b1056dfc575f2 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Sat, 29 Mar 2025 02:46:57 +0600 Subject: [PATCH 01/18] feat: add murena sso migration This commit introduces a new service to handle Murena SSO migration, including: - **MurenaSsoMigrationService:** A new service that removes the old eelo account and opens the new e account login form. - **BootCompletedReceiver:** Adds a check for Murena SSO accounts on boot and displays a notification if migration is needed. - **Notification:** Adds a new notification to switch to the new OpenID login. - **AccountDetailsFragment:** Modifies to manage Murena SSO account details. - **AndroidManifest:** Adds new service, and permissions. - **String:** Adds new string for the notification text and account type. - **NotificationUtils:** Adds new notification type. - Notify eDrive. --- app/src/main/AndroidManifest.xml | 7 +- .../davdroid/MurenaSsoMigrationService.kt | 121 ++++++++++++++++++ .../receiver/BootCompletedReceiver.kt | 101 ++++++++++++++- .../bitfire/davdroid/ui/NotificationUtils.kt | 3 +- .../ui/setup/AccountDetailsFragment.kt | 24 +++- app/src/main/res/values/strings.xml | 2 + 6 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bce6ab210..c42a0cd54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + + @@ -785,4 +788,4 @@ - \ No newline at end of file + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt new file mode 100644 index 000000000..56c931191 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt @@ -0,0 +1,121 @@ +/* + * 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 . + * + */ +package at.bitfire.davdroid + +import android.accounts.AccountManager +import android.accounts.AccountManagerFuture +import android.app.Service +import android.content.ComponentName +import android.content.Context +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.content.ContextCompat +import at.bitfire.davdroid.log.Logger +import org.apache.commons.lang3.NotImplementedException + +class MurenaSsoMigrationService : Service() { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + notifyEDrive() + + val context = this.applicationContext + + val accountManager = AccountManager.get(context) + val eAccountType = context.getString(R.string.eelo_account_type) + + val accountRemoved = removeAccount(eAccountType, accountManager) + + if (accountRemoved) { + openEAccountLoginForm(accountManager, eAccountType, context) + } + + return START_STICKY + } + + private fun notifyEDrive() { + val isMigrationRunning = true + + val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION") + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + intent.component = + ComponentName( + getString(R.string.e_drive_package_name), + "foundation.e.drive.murenasso.SsoMigrationReceiver" + ) + intent.putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) + + applicationContext.sendBroadcast(intent) + } + + private fun removeAccount(accountType: String, accountManager: AccountManager): Boolean { + val account = accountManager.getAccountsByType(accountType).firstOrNull() + + return account?.let { + accountManager.removeAccountExplicitly(it) + } ?: false + } + + private fun openEAccountLoginForm( + accountManager: AccountManager, + eAccountType: String, + context: Context + ) { + accountManager.addAccount( + eAccountType, + null, + null, + null, + null, + { future -> + val intent = getIntentFromFuture(future) + + intent?.let { + // Calling startActivity() from outside of an Activity context + // requires the FLAG_ACTIVITY_NEW_TASK flag + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ContextCompat.startActivity(context, it, null) + } + }, + null + ) + } + + private fun getIntentFromFuture(future: AccountManagerFuture): 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 + } + + } + + override fun onBind(intent: Intent?): IBinder? { + throw NotImplementedException() + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index d76975081..600408d3e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -4,12 +4,30 @@ package at.bitfire.davdroid.receiver +import android.Manifest.permission.POST_NOTIFICATIONS +import android.accounts.Account +import android.accounts.AccountManager +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.MurenaSsoMigrationService import at.bitfire.davdroid.log.Logger 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_SWITCH_TO_OPENID +import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible +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 @@ -20,14 +38,85 @@ import at.bitfire.davdroid.syncadapter.AccountUtils */ class BootCompletedReceiver: BroadcastReceiver() { + private val job = SupervisorJob() + private val scope = CoroutineScope(job + Dispatchers.IO) + 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() - AccountUtils.getMainAccounts(context) - .forEach { - val accountSettings = AccountSettings(context, it) - accountSettings.initSync() + + val pendingResult = goAsync() + scope.launch { + try { + val eAccountType = context.getString(R.string.eelo_account_type) + val accountManager = AccountManager.get(context) + val canNotify = canNotify(context) + + AccountUtils.getMainAccounts(context) + .forEach { + val needRelogin = + eAccountType == it.type && !isLoggedWithMurenaSso(it, accountManager) + + if (canNotify && needRelogin) { + notifySwitchToOpenId(it, context) + } else { // sync intervals are checked in App.onCreate() + val accountSettings = AccountSettings(context, it) + accountSettings.initSync() + } + } + } finally { + pendingResult.finish() + job.cancel() } + } } -} \ No newline at end of file + private fun isLoggedWithMurenaSso(account: Account, accountManager: AccountManager): Boolean { + val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null + val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() + return isPasswordNull && hasAuthStateData + } + + private fun notifySwitchToOpenId(account: Account, context: Context) { + val title = context.getString(R.string.notification_account_title, account.name) + val contentText = context.getString(R.string.notification_switch_to_openId_text) + val intent = generateReloginIntent(context) + + val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) + .setOngoing(true) // TODO: Does it work? + .setContentTitle(title) + .setContentText(contentText) + .setSmallIcon(R.drawable.ic_info) + .setContentIntent(intent) + .setAutoCancel(false) + .build() + + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.notifyIfPossible(NOTIFICATION_TAG, NOTIFY_SWITCH_TO_OPENID, notification) + } + + private fun generateReloginIntent(context: Context): PendingIntent { + val reloginIntent = Intent(context, MurenaSsoMigrationService::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + return PendingIntent.getService(context, + 0, + reloginIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } + + private fun canNotify(context: Context): Boolean { + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + val permission = POST_NOTIFICATIONS + if (context.checkSelfPermission(permission) != PERMISSION_GRANTED) { + Logger.log.warning("Missing notification permission") + return false + } + } + return true + } + + companion object { + private const val NOTIFICATION_TAG = "SWITCH_TO_OPENID" + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt index e41544d53..70f37e5aa 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt @@ -34,6 +34,7 @@ object NotificationUtils { const val NOTIFY_SYNC_EXPEDITED = 14 const val NOTIFY_TASKS_PROVIDER_TOO_OLD = 20 const val NOTIFY_PERMISSIONS = 21 + const val NOTIFY_SWITCH_TO_OPENID = 22 const val NOTIFY_LICENSE = 100 @@ -92,4 +93,4 @@ object NotificationUtils { fun NotificationManagerCompat.notifyIfPossible(id: Int, notification: Notification) = notifyIfPossible(null, id, notification) -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index a7cf8c1d9..9dd2fb503 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -13,7 +13,6 @@ import android.content.ComponentName import android.content.ContentResolver import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle import android.provider.CalendarContract import android.text.Editable @@ -23,6 +22,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast +import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -31,11 +31,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.viewModelScope -import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.Constants import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R -import at.bitfire.davdroid.authorization.IdentityProvider import at.bitfire.davdroid.databinding.LoginAccountDetailsBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Credentials @@ -60,8 +58,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch -import net.openid.appauth.AuthorizationService -import net.openid.appauth.EndSessionRequest import java.util.logging.Level import javax.inject.Inject @@ -197,6 +193,24 @@ class AccountDetailsFragment : Fragment() { if (requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) == getString(R.string.eelo_account_type)) { notifyEdrive(name) + + // TODO: Check if SSO is activated + // TODO: Move to better place + val isMigrationRunning = false + + val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION") + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + intent.component = + ComponentName( + getString(R.string.e_drive_package_name), + "foundation.e.drive.murenasso.SsoMigrationReceiver" + ) + intent.putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) + + requireContext().sendBroadcast(intent) + + NotificationManagerCompat.from(requireContext().applicationContext) + .cancelAll() // FIXME: Why cancel() not working? } handlePostAuthOperations() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68501ba08..68d4c49e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,8 @@ Non-fatal synchronization problems like certain invalid files Network and I/O errors Timeouts, connection problems, etc. (often temporary) + Your account %1$s + A new login service for a better experience is available. Tap the notification to start using it. Your data. Your choice. -- GitLab From 198d21548bf30d731893e8b8627bbcb5aece83ca Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 7 Apr 2025 13:22:41 +0600 Subject: [PATCH 02/18] fix: open login form on notification click in absence of an account on the device Fix opening of login form from clicking on notification even if there's no account on the device. --- .../davdroid/MurenaSsoMigrationService.kt | 16 ++++------------ .../davdroid/receiver/BootCompletedReceiver.kt | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt index 56c931191..3e27844c8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt @@ -37,15 +37,10 @@ class MurenaSsoMigrationService : Service() { notifyEDrive() val context = this.applicationContext - val accountManager = AccountManager.get(context) val eAccountType = context.getString(R.string.eelo_account_type) - - val accountRemoved = removeAccount(eAccountType, accountManager) - - if (accountRemoved) { - openEAccountLoginForm(accountManager, eAccountType, context) - } + removeAccount(eAccountType, accountManager) + openEAccountLoginForm(accountManager, eAccountType, context) return START_STICKY } @@ -65,12 +60,9 @@ class MurenaSsoMigrationService : Service() { applicationContext.sendBroadcast(intent) } - private fun removeAccount(accountType: String, accountManager: AccountManager): Boolean { + private fun removeAccount(accountType: String, accountManager: AccountManager) { val account = accountManager.getAccountsByType(accountType).firstOrNull() - - return account?.let { - accountManager.removeAccountExplicitly(it) - } ?: false + account?.let { accountManager.removeAccountExplicitly(it) } } private fun openEAccountLoginForm( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 600408d3e..683e2dbf5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -82,7 +82,7 @@ class BootCompletedReceiver: BroadcastReceiver() { val intent = generateReloginIntent(context) val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) - .setOngoing(true) // TODO: Does it work? + .setOngoing(true) .setContentTitle(title) .setContentText(contentText) .setSmallIcon(R.drawable.ic_info) -- GitLab From 46271e200881a4279abb4d94826f949d86a061c7 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 7 Apr 2025 14:31:02 +0600 Subject: [PATCH 03/18] feat: apply check for Murena base url --- .../receiver/BootCompletedReceiver.kt | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 683e2dbf5..506492009 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -15,8 +15,9 @@ 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.BuildConfig import at.bitfire.davdroid.MurenaSsoMigrationService +import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils @@ -24,6 +25,7 @@ import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.CHANNEL_GENERAL import at.bitfire.davdroid.ui.NotificationUtils.NOTIFY_SWITCH_TO_OPENID import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible +import com.owncloud.android.lib.common.accounts.AccountUtils.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -47,19 +49,15 @@ class BootCompletedReceiver: BroadcastReceiver() { val pendingResult = goAsync() scope.launch { try { - val eAccountType = context.getString(R.string.eelo_account_type) - val accountManager = AccountManager.get(context) - val canNotify = canNotify(context) - AccountUtils.getMainAccounts(context) - .forEach { - val needRelogin = - eAccountType == it.type && !isLoggedWithMurenaSso(it, accountManager) + .forEach { account -> + val canMigrateToMurenaSso = isMurenaAccount(context, account) && + !isLoggedInWithMurenaSso(context, account) - if (canNotify && needRelogin) { - notifySwitchToOpenId(it, context) + if (hasNotificationPermission(context) && canMigrateToMurenaSso) { + notifySsoMigration(account, context) } else { // sync intervals are checked in App.onCreate() - val accountSettings = AccountSettings(context, it) + val accountSettings = AccountSettings(context, account) accountSettings.initSync() } } @@ -70,16 +68,35 @@ class BootCompletedReceiver: BroadcastReceiver() { } } - private fun isLoggedWithMurenaSso(account: Account, accountManager: AccountManager): Boolean { + private fun isMurenaAccount(context: Context, account: Account): Boolean { + val accountManager = AccountManager.get(context) + val baseUrlData = + accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false + val baseUrl = sanitizeBaseUrl(baseUrlData) + val murenaBaseUrl = sanitizeBaseUrl(BuildConfig.MURENA_BASE_URL) + + // User can have their own Murena account set up with custom Nextcloud instance, + // so a check for base URL is necessary. + val isMurenaCloud = baseUrl == murenaBaseUrl + val isMurenaAccountType = account.type == context.getString(R.string.eelo_account_type) + + return isMurenaCloud && isMurenaAccountType + } + + private fun sanitizeBaseUrl(url: String) = AccountUtils.getOwnCloudBaseUrl(url) + + private fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { + val accountManager = AccountManager.get(context) val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() + return isPasswordNull && hasAuthStateData } - private fun notifySwitchToOpenId(account: Account, context: Context) { + private fun notifySsoMigration(account: Account, context: Context) { val title = context.getString(R.string.notification_account_title, account.name) val contentText = context.getString(R.string.notification_switch_to_openId_text) - val intent = generateReloginIntent(context) + val intent = createSsoMigrationIntent(context) val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) .setOngoing(true) @@ -94,7 +111,7 @@ class BootCompletedReceiver: BroadcastReceiver() { notificationManager.notifyIfPossible(NOTIFICATION_TAG, NOTIFY_SWITCH_TO_OPENID, notification) } - private fun generateReloginIntent(context: Context): PendingIntent { + private fun createSsoMigrationIntent(context: Context): PendingIntent { val reloginIntent = Intent(context, MurenaSsoMigrationService::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } @@ -105,7 +122,7 @@ class BootCompletedReceiver: BroadcastReceiver() { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } - private fun canNotify(context: Context): Boolean { + private fun hasNotificationPermission(context: Context): Boolean { if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { val permission = POST_NOTIFICATIONS if (context.checkSelfPermission(permission) != PERMISSION_GRANTED) { -- GitLab From a8dab6daac901cd4aedb97b25ff70a05995f5f31 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 7 Apr 2025 14:33:00 +0600 Subject: [PATCH 04/18] fix: apply correct file name for WebViewUtils --- .../android/utils/{AccountManagerUtils.kt => WebViewUtils.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/nextcloud/android/utils/{AccountManagerUtils.kt => WebViewUtils.kt} (100%) diff --git a/app/src/main/java/com/nextcloud/android/utils/AccountManagerUtils.kt b/app/src/main/java/com/nextcloud/android/utils/WebViewUtils.kt similarity index 100% rename from app/src/main/java/com/nextcloud/android/utils/AccountManagerUtils.kt rename to app/src/main/java/com/nextcloud/android/utils/WebViewUtils.kt -- GitLab From 0d61c3a0bf58d7e33b3364460195ef80da195e58 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 7 Apr 2025 18:11:14 +0600 Subject: [PATCH 05/18] refactor: improve code organization --- app/src/main/AndroidManifest.xml | 2 +- .../MurenaSsoMigrationPreferences.kt | 55 +++++++++++++ .../MurenaSsoMigrationService.kt | 82 ++++++++++++------- .../receiver/BootCompletedReceiver.kt | 27 +++--- .../davdroid/syncadapter/AccountUtils.kt | 8 ++ .../bitfire/davdroid/ui/NotificationUtils.kt | 2 +- .../ui/setup/AccountDetailsFragment.kt | 60 +++++++++----- 7 files changed, 166 insertions(+), 70 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt rename app/src/main/kotlin/at/bitfire/davdroid/{ => murenasso}/MurenaSsoMigrationService.kt (55%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c42a0cd54..2556087de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -214,7 +214,7 @@ . + */ + +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" + + @JvmStatic + fun isSsoMigrationRunning(context: Context): Boolean { + val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + return preferences.getBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false) + } + + @JvmStatic + fun updateSsoMigrationStatus(context: Context, status: MigrationStatus) { + val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + when (status) { + MigrationStatus.InProgress -> preferences.edit { + putBoolean( + KEY_IS_MURENA_SSO_MIGRATION_RUNNING, true + ) + } + + MigrationStatus.Completed -> preferences.edit { + putBoolean( + KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false + ) + } + } + } + + enum class MigrationStatus { + InProgress, Completed + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt similarity index 55% rename from app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt rename to app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index 3e27844c8..b4331cd7d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -15,61 +15,76 @@ * along with this program. If not, see . * */ -package at.bitfire.davdroid +package at.bitfire.davdroid.murenasso import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.app.Service import android.content.ComponentName -import android.content.Context 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 org.apache.commons.lang3.NotImplementedException +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences.MigrationStatus +import at.bitfire.davdroid.ui.NotificationUtils class MurenaSsoMigrationService : Service() { - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - notifyEDrive() + companion object { + const val NOTIFICATION_TAG_MURENA_SSO = "MIGRATE_TO_MURENA_SSO" + private const val ACTION_INTENT_MURENA_SSO_MIGRATION = + "foundation.e.drive.action.MURENA_SSO_MIGRATION" + private const val CLASS_NAME_EDRIVE = "foundation.e.drive.murenasso.SsoMigrationReceiver" + } - val context = this.applicationContext - val accountManager = AccountManager.get(context) - val eAccountType = context.getString(R.string.eelo_account_type) - removeAccount(eAccountType, accountManager) - openEAccountLoginForm(accountManager, eAccountType, context) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + updateMigrationStatus() + notifyEdrive() + removeAccount() + openMurenaLoginUi() return START_STICKY } - private fun notifyEDrive() { - val isMigrationRunning = true - - val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION") - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - intent.component = - ComponentName( - getString(R.string.e_drive_package_name), - "foundation.e.drive.murenasso.SsoMigrationReceiver" - ) - intent.putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) + private fun updateMigrationStatus() { + MurenaSsoMigrationPreferences.updateSsoMigrationStatus( + applicationContext, + MigrationStatus.InProgress + ) + } + private fun notifyEdrive() { + val intent = createIntentForEdrive() applicationContext.sendBroadcast(intent) } - private fun removeAccount(accountType: String, accountManager: AccountManager) { + private fun createIntentForEdrive(): Intent { + val intent = Intent(ACTION_INTENT_MURENA_SSO_MIGRATION).apply { + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + component = ComponentName(getString(R.string.e_drive_package_name), CLASS_NAME_EDRIVE) + val isMigrationRunning = true + putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) + } + + return intent + } + + private fun removeAccount() { + val accountManager = AccountManager.get(applicationContext) + val accountType = applicationContext.getString(R.string.eelo_account_type) val account = accountManager.getAccountsByType(accountType).firstOrNull() account?.let { accountManager.removeAccountExplicitly(it) } } - private fun openEAccountLoginForm( - accountManager: AccountManager, - eAccountType: String, - context: Context - ) { + private fun openMurenaLoginUi() { + val accountManager = AccountManager.get(applicationContext) + val eAccountType = applicationContext.getString(R.string.eelo_account_type) + accountManager.addAccount( eAccountType, null, @@ -83,7 +98,7 @@ class MurenaSsoMigrationService : Service() { // Calling startActivity() from outside of an Activity context // requires the FLAG_ACTIVITY_NEW_TASK flag it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ContextCompat.startActivity(context, it, null) + ContextCompat.startActivity(applicationContext, it, null) } }, null @@ -108,6 +123,15 @@ class MurenaSsoMigrationService : Service() { } override fun onBind(intent: Intent?): IBinder? { - throw NotImplementedException() + 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() } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 506492009..974f204b7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -16,14 +16,15 @@ import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.BuildConfig -import at.bitfire.davdroid.MurenaSsoMigrationService +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService.Companion.NOTIFICATION_TAG_MURENA_SSO import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger 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_SWITCH_TO_OPENID +import at.bitfire.davdroid.ui.NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import com.owncloud.android.lib.common.accounts.AccountUtils.* import kotlinx.coroutines.CoroutineScope @@ -51,8 +52,9 @@ class BootCompletedReceiver: BroadcastReceiver() { try { AccountUtils.getMainAccounts(context) .forEach { account -> - val canMigrateToMurenaSso = isMurenaAccount(context, account) && - !isLoggedInWithMurenaSso(context, account) + val canMigrateToMurenaSso = + isMurenaAccount(context, account) && + isNotUsingMurenaSso(context, account) if (hasNotificationPermission(context) && canMigrateToMurenaSso) { notifySsoMigration(account, context) @@ -68,6 +70,9 @@ class BootCompletedReceiver: BroadcastReceiver() { } } + private fun isNotUsingMurenaSso(context: Context, account: Account) = + !AccountUtils.isLoggedInWithMurenaSso(context, account) + private fun isMurenaAccount(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val baseUrlData = @@ -85,14 +90,6 @@ class BootCompletedReceiver: BroadcastReceiver() { private fun sanitizeBaseUrl(url: String) = AccountUtils.getOwnCloudBaseUrl(url) - private fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { - val accountManager = AccountManager.get(context) - val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null - val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() - - return isPasswordNull && hasAuthStateData - } - private fun notifySsoMigration(account: Account, context: Context) { val title = context.getString(R.string.notification_account_title, account.name) val contentText = context.getString(R.string.notification_switch_to_openId_text) @@ -108,7 +105,7 @@ class BootCompletedReceiver: BroadcastReceiver() { .build() val notificationManager = NotificationManagerCompat.from(context) - notificationManager.notifyIfPossible(NOTIFICATION_TAG, NOTIFY_SWITCH_TO_OPENID, notification) + notificationManager.notifyIfPossible(NOTIFICATION_TAG_MURENA_SSO, NOTIFY_MIGRATE_TO_MURENA_SSO, notification) } private fun createSsoMigrationIntent(context: Context): PendingIntent { @@ -132,8 +129,4 @@ class BootCompletedReceiver: BroadcastReceiver() { } return true } - - companion object { - private const val NOTIFICATION_TAG = "SWITCH_TO_OPENID" - } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index d94c06b0c..615649153 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -164,4 +164,12 @@ object AccountUtils { return null } + + fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { + val accountManager = AccountManager.get(context) + val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null + val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() + + return isPasswordNull && hasAuthStateData + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt index 70f37e5aa..047bf4e68 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt @@ -34,7 +34,7 @@ object NotificationUtils { const val NOTIFY_SYNC_EXPEDITED = 14 const val NOTIFY_TASKS_PROVIDER_TOO_OLD = 20 const val NOTIFY_PERMISSIONS = 21 - const val NOTIFY_SWITCH_TO_OPENID = 22 + const val NOTIFY_MIGRATE_TO_MURENA_SSO = 22 const val NOTIFY_LICENSE = 100 diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 9dd2fb503..3164a2be6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -22,7 +22,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast -import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -40,6 +39,9 @@ import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences.MigrationStatus.Completed +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.servicedetection.DavResourceFinder import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker @@ -192,27 +194,9 @@ class AccountDetailsFragment : Fragment() { } if (requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) == getString(R.string.eelo_account_type)) { - notifyEdrive(name) - - // TODO: Check if SSO is activated - // TODO: Move to better place - val isMigrationRunning = false - - val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION") - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - intent.component = - ComponentName( - getString(R.string.e_drive_package_name), - "foundation.e.drive.murenasso.SsoMigrationReceiver" - ) - intent.putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) - - requireContext().sendBroadcast(intent) - - NotificationManagerCompat.from(requireContext().applicationContext) - .cancelAll() // FIXME: Why cancel() not working? + notifyEdriveWithAccountAdded(name) + handlePostMurenaSsoMigrationOperations() } - handlePostAuthOperations() } }) @@ -222,7 +206,39 @@ class AccountDetailsFragment : Fragment() { return v.root } - private fun notifyEdrive(name: String) { + private fun handlePostMurenaSsoMigrationOperations() { + val authState = requireActivity().intent.getStringExtra(LoginActivity.AUTH_STATE) + val isMigratedToMurenaSso = !authState.isNullOrEmpty() + val isMigrationRunning = + MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext()) + + if (isMigrationRunning && isMigratedToMurenaSso) { + notifyEdriveWithSsoMigrationComplete() + stopMurenaSsoMigrationService() + MurenaSsoMigrationPreferences.updateSsoMigrationStatus(requireContext(), Completed) + Logger.log.info("Murena SSO migration is complete.") + } + } + + private fun notifyEdriveWithSsoMigrationComplete() { + val isMigrationRunning = false + val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION").apply { + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + component = ComponentName( + getString(R.string.e_drive_package_name), + "foundation.e.drive.murenasso.SsoMigrationReceiver" + ) + putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) + } + requireContext().sendBroadcast(intent) + } + + private fun stopMurenaSsoMigrationService() { + val serviceIntent = Intent(requireContext(), MurenaSsoMigrationService::class.java) + requireContext().stopService(serviceIntent) + } + + private fun notifyEdriveWithAccountAdded(name: String) { val intent = Intent("foundation.e.drive.action.ADD_ACCOUNT") intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) intent.component = -- GitLab From 8160ee471be85ed2bcab3d932f2f6d92d6bb31b3 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 7 Apr 2025 18:20:54 +0600 Subject: [PATCH 06/18] refactor: enable OpenID Connect by default --- .../at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt index 6e785ffc6..9b05b2e60 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt @@ -27,7 +27,7 @@ class EeloAuthenticatorModel(application: Application) : AndroidViewModel(applic companion object { // as https://gitlab.e.foundation/e/backlog/-/issues/6287 is blocked, the openId implementation is not ready yet. // But we want to push the changes so later we won't face any conflict. So we are disabling the openId feature for now. - const val ENABLE_OIDC_SUPPORT = false + const val ENABLE_OIDC_SUPPORT = true } private var initialized = false -- GitLab From e94e6508dd5a6195664c8f596c3e1e6983501efc Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 10 Apr 2025 14:31:22 +0600 Subject: [PATCH 07/18] refactor: delete previous non-SSO Murena account only when migration to SSO is successful --- .../murenasso/MurenaSsoMigrationService.kt | 8 ----- .../receiver/BootCompletedReceiver.kt | 31 +++---------------- .../davdroid/syncadapter/AccountUtils.kt | 24 +++++++++++++- .../ui/setup/AccountDetailsFragment.kt | 26 +++++++++++++--- .../ui/setup/DetectConfigurationFragment.kt | 11 ++++--- .../ui/setup/EeloAuthenticatorFragment.kt | 4 +++ .../ui/setup/MurenaOpenIdAuthFragment.kt | 6 ++++ 7 files changed, 66 insertions(+), 44 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index b4331cd7d..1255fad98 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -45,7 +45,6 @@ class MurenaSsoMigrationService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { updateMigrationStatus() notifyEdrive() - removeAccount() openMurenaLoginUi() return START_STICKY @@ -74,13 +73,6 @@ class MurenaSsoMigrationService : Service() { return intent } - private fun removeAccount() { - val accountManager = AccountManager.get(applicationContext) - val accountType = applicationContext.getString(R.string.eelo_account_type) - val account = accountManager.getAccountsByType(accountType).firstOrNull() - account?.let { accountManager.removeAccountExplicitly(it) } - } - private fun openMurenaLoginUi() { val accountManager = AccountManager.get(applicationContext) val eAccountType = applicationContext.getString(R.string.eelo_account_type) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 974f204b7..54ca87138 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.receiver import android.Manifest.permission.POST_NOTIFICATIONS import android.accounts.Account -import android.accounts.AccountManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context @@ -15,18 +14,16 @@ 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.BuildConfig -import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService -import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService.Companion.NOTIFICATION_TAG_MURENA_SSO 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 com.owncloud.android.lib.common.accounts.AccountUtils.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -53,8 +50,8 @@ class BootCompletedReceiver: BroadcastReceiver() { AccountUtils.getMainAccounts(context) .forEach { account -> val canMigrateToMurenaSso = - isMurenaAccount(context, account) && - isNotUsingMurenaSso(context, account) + AccountUtils.isMurenaAccount(context, account) && + AccountUtils.isNotUsingMurenaSso(context, account) if (hasNotificationPermission(context) && canMigrateToMurenaSso) { notifySsoMigration(account, context) @@ -70,26 +67,6 @@ class BootCompletedReceiver: BroadcastReceiver() { } } - private fun isNotUsingMurenaSso(context: Context, account: Account) = - !AccountUtils.isLoggedInWithMurenaSso(context, account) - - private fun isMurenaAccount(context: Context, account: Account): Boolean { - val accountManager = AccountManager.get(context) - val baseUrlData = - accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false - val baseUrl = sanitizeBaseUrl(baseUrlData) - val murenaBaseUrl = sanitizeBaseUrl(BuildConfig.MURENA_BASE_URL) - - // User can have their own Murena account set up with custom Nextcloud instance, - // so a check for base URL is necessary. - val isMurenaCloud = baseUrl == murenaBaseUrl - val isMurenaAccountType = account.type == context.getString(R.string.eelo_account_type) - - return isMurenaCloud && isMurenaAccountType - } - - private fun sanitizeBaseUrl(url: String) = AccountUtils.getOwnCloudBaseUrl(url) - private fun notifySsoMigration(account: Account, context: Context) { val title = context.getString(R.string.notification_account_title, account.name) val contentText = context.getString(R.string.notification_switch_to_openId_text) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 615649153..abcb1e906 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -8,11 +8,13 @@ import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.os.Bundle +import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.setAndVerifyUserData import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants object AccountUtils { @@ -165,7 +167,27 @@ object AccountUtils { return null } - fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { + fun isMurenaAccount(context: Context, account: Account): Boolean { + val accountManager = AccountManager.get(context) + val baseUrlData = + accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false + val baseUrl = sanitizeBaseUrl(baseUrlData) + val murenaBaseUrl = sanitizeBaseUrl(BuildConfig.MURENA_BASE_URL) + + // User can have their own Murena account set up with custom Nextcloud instance, + // so a check for base URL is necessary. + val isMurenaCloud = baseUrl == murenaBaseUrl + val isMurenaAccountType = account.type == context.getString(R.string.eelo_account_type) + + return isMurenaCloud && isMurenaAccountType + } + + fun isNotUsingMurenaSso(context: Context, account: Account) = + !isLoggedInWithMurenaSso(context, account) + + private fun sanitizeBaseUrl(url: String) = getOwnCloudBaseUrl(url) + + private fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 3164a2be6..928d94c14 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -48,6 +48,8 @@ import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils +import at.bitfire.davdroid.syncadapter.AccountUtils.isMurenaAccount +import at.bitfire.davdroid.syncadapter.AccountUtils.isNotUsingMurenaSso import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.util.AuthStatePrefUtils @@ -172,6 +174,15 @@ class AccountDetailsFragment : Fragment() { val idx = v.contactGroupMethod.selectedItemPosition val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx] + val murenaAccountBeforeSsoMigration = AccountUtils.getMainAccounts(requireContext()) + .firstOrNull { account -> + isMurenaAccount(requireContext(), account) && + isNotUsingMurenaSso(requireContext(), account) + } + murenaAccountBeforeSsoMigration?.let { + Logger.log.info("Found non-SSO Murena account: $it") + } + model.createAccount( requireActivity(), name, @@ -195,7 +206,7 @@ class AccountDetailsFragment : Fragment() { if (requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) == getString(R.string.eelo_account_type)) { notifyEdriveWithAccountAdded(name) - handlePostMurenaSsoMigrationOperations() + handlePostMurenaSsoMigrationOperations(murenaAccountBeforeSsoMigration) } handlePostAuthOperations() } @@ -206,16 +217,23 @@ class AccountDetailsFragment : Fragment() { return v.root } - private fun handlePostMurenaSsoMigrationOperations() { - val authState = requireActivity().intent.getStringExtra(LoginActivity.AUTH_STATE) - val isMigratedToMurenaSso = !authState.isNullOrEmpty() + private fun handlePostMurenaSsoMigrationOperations(accountToDelete: Account?) { val isMigrationRunning = MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext()) + val authState = requireActivity().intent.getStringExtra(LoginActivity.AUTH_STATE) + val isMigratedToMurenaSso = !authState.isNullOrEmpty() + if (isMigrationRunning && isMigratedToMurenaSso) { + if (accountToDelete != null) { + AccountManager.get(requireContext()).removeAccountExplicitly(accountToDelete) + Logger.log.info("Deleted previous Murena account: $accountToDelete") + } + notifyEdriveWithSsoMigrationComplete() stopMurenaSsoMigrationService() MurenaSsoMigrationPreferences.updateSsoMigrationStatus(requireContext(), Completed) + Logger.log.info("Murena SSO migration is complete.") } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt index be7d2750e..3c80ddf72 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt @@ -21,6 +21,7 @@ import at.bitfire.davdroid.ECloudAccountHelper import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences import at.bitfire.davdroid.servicedetection.DavResourceFinder import at.bitfire.davdroid.ui.DebugInfoActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -40,9 +41,11 @@ class DetectConfigurationFragment: Fragment() { val accountType = requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) - if (model.blockProceedWithLogin(accountType)) { - ECloudAccountHelper.showMultipleECloudAccountNotAcceptedDialog(requireActivity()) - return + if (!MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext())) { + if (model.blockProceedWithLogin(accountType)) { + ECloudAccountHelper.showMultipleECloudAccountNotAcceptedDialog(requireActivity()) + return + } } val blockOnUnauthorizedException = (accountType == getString(R.string.eelo_account_type)) && !EeloAuthenticatorModel.ENABLE_OIDC_SUPPORT @@ -159,4 +162,4 @@ class DetectConfigurationFragment: Fragment() { } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt index 47bbafcf0..8819b753d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt @@ -37,6 +37,7 @@ import at.bitfire.davdroid.ECloudAccountHelper import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.FragmentEeloAuthenticatorBinding import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences import at.bitfire.davdroid.ui.ShowUrlActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText @@ -119,6 +120,9 @@ class EeloAuthenticatorFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext())) { + return + } if (model.blockProceedWithLogin()) { ECloudAccountHelper.showMultipleECloudAccountNotAcceptedDialog(requireActivity()) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/MurenaOpenIdAuthFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/MurenaOpenIdAuthFragment.kt index ed1bea7a0..b7b37c9a6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/MurenaOpenIdAuthFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/MurenaOpenIdAuthFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.View import at.bitfire.davdroid.ECloudAccountHelper import at.bitfire.davdroid.authorization.IdentityProvider +import at.bitfire.davdroid.murenasso.MurenaSsoMigrationPreferences import org.json.JSONObject class MurenaOpenIdAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvider.MURENA) { @@ -27,6 +28,11 @@ class MurenaOpenIdAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvid override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext())) { + startAuthFLow() + return + } + if (!isAuthFlowComplete() && ECloudAccountHelper.alreadyHasECloudAccount(requireContext())) { ECloudAccountHelper.showMultipleECloudAccountNotAcceptedDialog(requireActivity()) return -- GitLab From 9a674528ec826755538346da01f118b8f68553fb Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 11 Apr 2025 18:03:52 +0600 Subject: [PATCH 08/18] refactor: update existing non-SSO Murena account with SSO-based configuration --- .../murenasso/MurenaSsoMigrationService.kt | 34 ++----- .../receiver/BootCompletedReceiver.kt | 6 +- .../davdroid/syncadapter/AccountUtils.kt | 5 +- .../ui/setup/AccountDetailsFragment.kt | 91 +++++++++---------- 4 files changed, 57 insertions(+), 79 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index 1255fad98..4160bac07 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -20,7 +20,6 @@ package at.bitfire.davdroid.murenasso import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.app.Service -import android.content.ComponentName import android.content.Intent import android.os.Build.VERSION import android.os.Build.VERSION_CODES @@ -37,14 +36,10 @@ class MurenaSsoMigrationService : Service() { companion object { const val NOTIFICATION_TAG_MURENA_SSO = "MIGRATE_TO_MURENA_SSO" - private const val ACTION_INTENT_MURENA_SSO_MIGRATION = - "foundation.e.drive.action.MURENA_SSO_MIGRATION" - private const val CLASS_NAME_EDRIVE = "foundation.e.drive.murenasso.SsoMigrationReceiver" } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { updateMigrationStatus() - notifyEdrive() openMurenaLoginUi() return START_STICKY @@ -57,22 +52,6 @@ class MurenaSsoMigrationService : Service() { ) } - private fun notifyEdrive() { - val intent = createIntentForEdrive() - applicationContext.sendBroadcast(intent) - } - - private fun createIntentForEdrive(): Intent { - val intent = Intent(ACTION_INTENT_MURENA_SSO_MIGRATION).apply { - addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - component = ComponentName(getString(R.string.e_drive_package_name), CLASS_NAME_EDRIVE) - val isMigrationRunning = true - putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) - } - - return intent - } - private fun openMurenaLoginUi() { val accountManager = AccountManager.get(applicationContext) val eAccountType = applicationContext.getString(R.string.eelo_account_type) @@ -85,13 +64,14 @@ class MurenaSsoMigrationService : Service() { null, { future -> val intent = getIntentFromFuture(future) - - intent?.let { - // Calling startActivity() from outside of an Activity context - // requires the FLAG_ACTIVITY_NEW_TASK flag - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ContextCompat.startActivity(applicationContext, it, null) + if (intent == null) { + stopSelf() // Stops the service and dismisses the notification as migration isn't possible. + return@addAccount } + // Calling startActivity() from outside of an Activity context + // requires the FLAG_ACTIVITY_NEW_TASK flag + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ContextCompat.startActivity(applicationContext, intent, null) }, null ) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 54ca87138..9c6ad8479 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -20,6 +20,8 @@ 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.syncadapter.AccountUtils.isLoggedInWithMurenaSso +import at.bitfire.davdroid.syncadapter.AccountUtils.isMurenaAccount 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 @@ -50,8 +52,8 @@ class BootCompletedReceiver: BroadcastReceiver() { AccountUtils.getMainAccounts(context) .forEach { account -> val canMigrateToMurenaSso = - AccountUtils.isMurenaAccount(context, account) && - AccountUtils.isNotUsingMurenaSso(context, account) + isMurenaAccount(context, account) && + !isLoggedInWithMurenaSso(context, account) if (hasNotificationPermission(context) && canMigrateToMurenaSso) { notifySsoMigration(account, context) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index abcb1e906..13f1371d4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -182,12 +182,9 @@ object AccountUtils { return isMurenaCloud && isMurenaAccountType } - fun isNotUsingMurenaSso(context: Context, account: Account) = - !isLoggedInWithMurenaSso(context, account) - private fun sanitizeBaseUrl(url: String) = getOwnCloudBaseUrl(url) - private fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { + fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null val isPasswordNull = accountManager.getPassword(account).isNullOrEmpty() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 928d94c14..300e023ab 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -48,8 +48,8 @@ import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils +import at.bitfire.davdroid.syncadapter.AccountUtils.isLoggedInWithMurenaSso import at.bitfire.davdroid.syncadapter.AccountUtils.isMurenaAccount -import at.bitfire.davdroid.syncadapter.AccountUtils.isNotUsingMurenaSso import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.util.AuthStatePrefUtils @@ -119,12 +119,12 @@ class AccountDetailsFragment : Fragment() { v.createAccountProgress.visibility = View.VISIBLE v.createAccount.visibility = View.GONE - model.createAccount( + model.createOrUpdateAccount( requireActivity(), name, loginModel.credentials, config, - GroupMethod.valueOf(groupMethodName) + GroupMethod.valueOf(groupMethodName), ).observe(viewLifecycleOwner, Observer { success -> if (success) { // close Create account activity @@ -174,21 +174,22 @@ class AccountDetailsFragment : Fragment() { val idx = v.contactGroupMethod.selectedItemPosition val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx] - val murenaAccountBeforeSsoMigration = AccountUtils.getMainAccounts(requireContext()) + val existingNonSsoMurenaAccount = AccountUtils.getMainAccounts(requireContext()) .firstOrNull { account -> isMurenaAccount(requireContext(), account) && - isNotUsingMurenaSso(requireContext(), account) + !isLoggedInWithMurenaSso(requireContext(), account) } - murenaAccountBeforeSsoMigration?.let { + existingNonSsoMurenaAccount?.let { Logger.log.info("Found non-SSO Murena account: $it") } - model.createAccount( + model.createOrUpdateAccount( requireActivity(), name, loginModel.credentials!!, config, - GroupMethod.valueOf(groupMethodName) + GroupMethod.valueOf(groupMethodName), + existingNonSsoMurenaAccount, ).observe(viewLifecycleOwner, Observer { success -> if (success) { Toast.makeText(context, R.string.message_account_added_successfully, Toast.LENGTH_LONG).show() @@ -205,8 +206,14 @@ class AccountDetailsFragment : Fragment() { } if (requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) == getString(R.string.eelo_account_type)) { - notifyEdriveWithAccountAdded(name) - handlePostMurenaSsoMigrationOperations(murenaAccountBeforeSsoMigration) + val isSsoMigrationRunning = + MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext()) + + if (isSsoMigrationRunning) { + handlePostMurenaSsoMigrationOperations() + } else { + notifyEdriveWithAccountAdded(name) + } } handlePostAuthOperations() } @@ -217,40 +224,17 @@ class AccountDetailsFragment : Fragment() { return v.root } - private fun handlePostMurenaSsoMigrationOperations(accountToDelete: Account?) { - val isMigrationRunning = - MurenaSsoMigrationPreferences.isSsoMigrationRunning(requireContext()) - + private fun handlePostMurenaSsoMigrationOperations() { val authState = requireActivity().intent.getStringExtra(LoginActivity.AUTH_STATE) val isMigratedToMurenaSso = !authState.isNullOrEmpty() - if (isMigrationRunning && isMigratedToMurenaSso) { - if (accountToDelete != null) { - AccountManager.get(requireContext()).removeAccountExplicitly(accountToDelete) - Logger.log.info("Deleted previous Murena account: $accountToDelete") - } - - notifyEdriveWithSsoMigrationComplete() + if (isMigratedToMurenaSso) { stopMurenaSsoMigrationService() MurenaSsoMigrationPreferences.updateSsoMigrationStatus(requireContext(), Completed) - Logger.log.info("Murena SSO migration is complete.") } } - private fun notifyEdriveWithSsoMigrationComplete() { - val isMigrationRunning = false - val intent = Intent("foundation.e.drive.action.MURENA_SSO_MIGRATION").apply { - addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - component = ComponentName( - getString(R.string.e_drive_package_name), - "foundation.e.drive.murenasso.SsoMigrationReceiver" - ) - putExtra(AccountManager.KEY_USERDATA, isMigrationRunning) - } - requireContext().sendBroadcast(intent) - } - private fun stopMurenaSsoMigrationService() { val serviceIntent = Intent(requireContext(), MurenaSsoMigrationService::class.java) requireContext().stopService(serviceIntent) @@ -320,7 +304,14 @@ class AccountDetailsFragment : Fragment() { * @param groupMethod Whether CardDAV contact groups are separate VCards or as contact categories * @return *true* if account creation was succesful; *false* otherwise (for instance because an account with this name already exists) */ - fun createAccount(activity: Activity, name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { + fun createOrUpdateAccount( + activity: Activity, + name: String, + credentials: Credentials?, + config: DavResourceFinder.Configuration, + groupMethod: GroupMethod, + accountToUpdate: Account? = null + ): LiveData { val result = MutableLiveData() viewModelScope.launch(Dispatchers.Default + NonCancellable) { var accountType = context.getString(R.string.account_type) @@ -352,23 +343,31 @@ class AccountDetailsFragment : Fragment() { } } - val account = Account(name, accountType) + val account = accountToUpdate ?: Account(name, accountType) - // create Android account val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies, config.calDAV?.emails?.firstOrNull()) AuthStatePrefUtils.saveAuthState(context, account, credentials?.authState?.jsonSerializeString()) - Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) - val accountManager = AccountManager.get(context) - if (!AccountUtils.createAccount(context, account, userData, credentials?.password)) { - if (accountType in AccountUtils.getOpenIdMainAccountTypes(context) && credentials?.authState != null) { - updateAuthState(userData, accountManager, account) - } else { - result.postValue(false) - return@launch + if (accountToUpdate != null) { + Logger.log.info("Updating auth state for existing non-SSO Murena account: $account") + updateAuthState(userData, accountManager, account) + } else { + if (!AccountUtils.createAccount( + context, + account, + userData, + credentials?.password + ) + ) { + if (accountType in AccountUtils.getOpenIdMainAccountTypes(context) && credentials?.authState != null) { + updateAuthState(userData, accountManager, account) + } else { + result.postValue(false) + return@launch + } } } -- GitLab From 28c5e3f679248d1043c44de62e15187ff57c822d Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 11 Apr 2025 18:10:30 +0600 Subject: [PATCH 09/18] refactor: remove unnecessary permission --- app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2556087de..7f3af0812 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,7 +30,6 @@ - Your data. Your choice. -- GitLab From c16ac19dceb29be90daf1e7860dbb3e5d6e22d4c Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 15 Apr 2025 17:30:40 +0600 Subject: [PATCH 11/18] refactor: improve code readability on BootCompletedReceiver --- .../murenasso/MurenaSsoMigrationService.kt | 4 +- .../receiver/BootCompletedReceiver.kt | 77 +++++++++++-------- .../davdroid/syncadapter/AccountUtils.kt | 2 + 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index 7b41bab08..ede040a6b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -40,9 +40,9 @@ class MurenaSsoMigrationService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val accountName = intent?.getStringExtra(LoginActivity.EXTRA_USERNAME) - updateMigrationStatus() + + val accountName = intent?.getStringExtra(LoginActivity.EXTRA_USERNAME) openMurenaLoginUi(accountName) return START_STICKY diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 153456b2e..3a3b9b6ed 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -40,29 +40,15 @@ import kotlinx.coroutines.launch * is started, it checks (and repairs, if necessary) the sync intervals in [App.onCreate]. */ class BootCompletedReceiver: BroadcastReceiver() { - - private val job = SupervisorJob() - private val scope = CoroutineScope(job + Dispatchers.IO) - override fun onReceive(context: Context, intent: Intent) { - Logger.log.info("Device has been rebooted; checking sync intervals etc.") - val pendingResult = goAsync() + val job = SupervisorJob() + val scope = CoroutineScope(job + Dispatchers.IO) + scope.launch { try { - AccountUtils.getMainAccounts(context) - .forEach { account -> - val canMigrateToMurenaSso = - isMurenaAccount(context, account) && - !isLoggedInWithMurenaSso(context, account) - - if (hasNotificationPermission(context) && canMigrateToMurenaSso) { - notifySsoMigration(account, context) - } else { // sync intervals are checked in App.onCreate() - val accountSettings = AccountSettings(context, account) - accountSettings.initSync() - } - } + initializeSync(context) + handleMurenaSsoMigration(context) } finally { pendingResult.finish() job.cancel() @@ -70,7 +56,37 @@ class BootCompletedReceiver: BroadcastReceiver() { } } - private fun notifySsoMigration(account: Account, context: Context) { + 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 { + isMurenaAccount(context, it) && !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) @@ -88,6 +104,16 @@ class BootCompletedReceiver: BroadcastReceiver() { 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) @@ -99,15 +125,4 @@ class BootCompletedReceiver: BroadcastReceiver() { serviceIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } - - private fun hasNotificationPermission(context: Context): Boolean { - if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - val permission = POST_NOTIFICATIONS - if (context.checkSelfPermission(permission) != PERMISSION_GRANTED) { - Logger.log.warning("Missing notification permission") - return false - } - } - return true - } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 13f1371d4..0bb30f569 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -169,8 +169,10 @@ object AccountUtils { fun isMurenaAccount(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) + val baseUrlData = accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false + val baseUrl = sanitizeBaseUrl(baseUrlData) val murenaBaseUrl = sanitizeBaseUrl(BuildConfig.MURENA_BASE_URL) -- GitLab From 84bec2ace57cdf8009236526bc92427e19d468ea Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 16 Apr 2025 11:14:11 +0600 Subject: [PATCH 12/18] chore: update notification title --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c901ae4b8..b13b238a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,7 +54,7 @@ Network and I/O errors Timeouts, connection problems, etc. (often temporary) Your account %1$s - Account update %1$s + Account update for %1$s A new login service for a better experience is available. Tap the notification to start using it. -- GitLab From a1dc8e71a34f964c2a73c2f541ee5f7d98d366b4 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 22 Apr 2025 20:16:52 +0600 Subject: [PATCH 13/18] refactor: resolve MR feedback --- .../receiver/BootCompletedReceiver.kt | 5 ++-- .../davdroid/settings/AccountSettings.kt | 2 +- .../davdroid/syncadapter/AccountUtils.kt | 26 +++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index 3a3b9b6ed..39b843b61 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -20,8 +20,6 @@ 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.syncadapter.AccountUtils.isLoggedInWithMurenaSso -import at.bitfire.davdroid.syncadapter.AccountUtils.isMurenaAccount 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 @@ -77,7 +75,8 @@ class BootCompletedReceiver: BroadcastReceiver() { private fun findAccountForSsoMigration(context: Context): Account? { return AccountUtils.getMainAccounts(context) .firstOrNull { - isMurenaAccount(context, it) && !isLoggedInWithMurenaSso(context, it) + AccountUtils.isMurenaAccount(context, it) && + !AccountUtils.isLoggedInWithMurenaSso(context, it) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index abbb5855e..c027c872c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -163,7 +163,7 @@ class AccountSettings( } if (!url.isNullOrEmpty()) { - val baseUrl = AccountUtils.getOwnCloudBaseUrl(url) + val baseUrl = AccountUtils.extractBaseUrl(url) bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, baseUrl) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 0bb30f569..17dfd311e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -133,12 +133,12 @@ object AccountUtils { return accounts } - fun getOwnCloudBaseUrl(baseURL: String): String { - if (baseURL.contains("/remote.php")) { - return baseURL.split("/remote.php")[0] + fun extractBaseUrl(url: String): String { + if (url.contains("/remote.php")) { + return url.split("/remote.php")[0] } - return baseURL + return url } fun getAccount(context: Context, userName: String?, requestedBaseUrl: String?): Account? { @@ -146,7 +146,7 @@ object AccountUtils { return null } - val baseUrl = getOwnCloudBaseUrl(requestedBaseUrl) + val baseUrl = extractBaseUrl(requestedBaseUrl) val accountManager = AccountManager.get(context.applicationContext) @@ -158,8 +158,8 @@ object AccountUtils { continue } - val url = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL) - if (url != null && getOwnCloudBaseUrl(url) == baseUrl) { + val url = accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) + if (url != null && extractBaseUrl(url) == baseUrl) { return account } } @@ -170,22 +170,20 @@ object AccountUtils { fun isMurenaAccount(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) - val baseUrlData = + val urlData = accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false - val baseUrl = sanitizeBaseUrl(baseUrlData) - val murenaBaseUrl = sanitizeBaseUrl(BuildConfig.MURENA_BASE_URL) + val baseUrl = extractBaseUrl(urlData) + val murenaBaseUrl = extractBaseUrl(BuildConfig.MURENA_BASE_URL) // User can have their own Murena account set up with custom Nextcloud instance, // so a check for base URL is necessary. - val isMurenaCloud = baseUrl == murenaBaseUrl - val isMurenaAccountType = account.type == context.getString(R.string.eelo_account_type) + val isMurenaCloud = (baseUrl == murenaBaseUrl) + val isMurenaAccountType = (account.type == context.getString(R.string.eelo_account_type)) return isMurenaCloud && isMurenaAccountType } - private fun sanitizeBaseUrl(url: String) = getOwnCloudBaseUrl(url) - fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val hasAuthStateData = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) != null -- GitLab From 70085cfcde0c84abc82d325fcb4bbbeb8d5ed6e1 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 23 Apr 2025 20:33:33 +0600 Subject: [PATCH 14/18] refactor: handle null check for account name --- .../davdroid/murenasso/MurenaSsoMigrationService.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index ede040a6b..99f472879 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -40,9 +40,13 @@ class MurenaSsoMigrationService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - updateMigrationStatus() - val accountName = intent?.getStringExtra(LoginActivity.EXTRA_USERNAME) + if (accountName.isNullOrBlank()) { + stopSelf() + return START_NOT_STICKY + } + + updateMigrationStatus() openMurenaLoginUi(accountName) return START_STICKY -- GitLab From ab9f322b8cd3e1862f73e3bca28a66277b69593c Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 23 Apr 2025 20:36:35 +0600 Subject: [PATCH 15/18] refactor: simplify checking of migration status using SharedPreference --- .../murenasso/MurenaSsoMigrationPreferences.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt index e4aea5761..9212a40a8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt @@ -34,19 +34,8 @@ object MurenaSsoMigrationPreferences { @JvmStatic fun updateSsoMigrationStatus(context: Context, status: MigrationStatus) { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - when (status) { - MigrationStatus.InProgress -> preferences.edit { - putBoolean( - KEY_IS_MURENA_SSO_MIGRATION_RUNNING, true - ) - } - - MigrationStatus.Completed -> preferences.edit { - putBoolean( - KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false - ) - } - } + val isRunning = (status == MigrationStatus.InProgress) + preferences.edit { putBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, isRunning) } } enum class MigrationStatus { -- GitLab From 177c63e595b51490a51aaf1c1af786fc196f948a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 23 Apr 2025 21:08:31 +0600 Subject: [PATCH 16/18] refactor: clear password for account after migrating to SSO --- .../at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 300e023ab..9a92e7699 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -354,6 +354,13 @@ class AccountDetailsFragment : Fragment() { if (accountToUpdate != null) { Logger.log.info("Updating auth state for existing non-SSO Murena account: $account") updateAuthState(userData, accountManager, account) + + // Clear password for SSO account + val authState = AuthStatePrefUtils.loadAuthState(context, name, accountType) + if (!authState.isNullOrBlank()) { + accountManager.clearPassword(account) + Logger.log.info("Cleared password for account: $account") + } } else { if (!AccountUtils.createAccount( context, -- GitLab From 8a90d8c8819cd5a09854bf16acd7b6e37b786f36 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 23 Apr 2025 21:13:19 +0600 Subject: [PATCH 17/18] refactor: remove unnecessary @JvmStatic annotations --- .../bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt index 9212a40a8..164df10f6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationPreferences.kt @@ -25,13 +25,11 @@ object MurenaSsoMigrationPreferences { private const val PREFERENCES_NAME = "murena_sso_migration" private const val KEY_IS_MURENA_SSO_MIGRATION_RUNNING = "is_sso_migration_running" - @JvmStatic fun isSsoMigrationRunning(context: Context): Boolean { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) return preferences.getBoolean(KEY_IS_MURENA_SSO_MIGRATION_RUNNING, false) } - @JvmStatic fun updateSsoMigrationStatus(context: Context, status: MigrationStatus) { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) val isRunning = (status == MigrationStatus.InProgress) -- GitLab From b8f254bd6cf286209865edbb043cc904648e2b58 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 24 Apr 2025 16:29:26 +0600 Subject: [PATCH 18/18] refactor: improve invocation point of updating migration status in SharedPref --- .../murenasso/MurenaSsoMigrationService.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt index 99f472879..b501a9b9c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/murenasso/MurenaSsoMigrationService.kt @@ -45,20 +45,11 @@ class MurenaSsoMigrationService : Service() { stopSelf() return START_NOT_STICKY } - - updateMigrationStatus() openMurenaLoginUi(accountName) return START_STICKY } - private fun updateMigrationStatus() { - MurenaSsoMigrationPreferences.updateSsoMigrationStatus( - applicationContext, - MigrationStatus.InProgress - ) - } - private fun openMurenaLoginUi(accountName: String?) { val accountManager = AccountManager.get(applicationContext) val eAccountType = applicationContext.getString(R.string.eelo_account_type) @@ -75,6 +66,9 @@ class MurenaSsoMigrationService : Service() { 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 { @@ -101,7 +95,13 @@ class MurenaSsoMigrationService : Service() { 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? { -- GitLab