diff --git a/app/build.gradle b/app/build.gradle index 0904d4364eacd09f6c8c0367d6eb9880d005981a..f23b9754c986316384437408cf60270e350acc1d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,7 +30,7 @@ android { setProperty "archivesBaseName", "davx5-ose-" + getVersionName() - minSdkVersion 24 // Android 7.0 + minSdkVersion 26 // Android 9.0 targetSdkVersion 33 // Android 13 buildConfigField "String", "userAgent", "\"AccountManager\"" @@ -156,6 +156,7 @@ configurations { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'androidx.activity:activity:1.9.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30e04639bf227a9af7c83a4dbc1829bca94a65c0..3a69080a3c91fd3158a6c87e65034094c9438585 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,118 +1,170 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:installLocation="internalOnly"> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + - - - - + - fine location (Android 10) + --> + + + + + + - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:supportsRtl="true" + android:theme="@style/AppTheme" + tools:ignore="UnusedAttribute"> + + - + tools:node="remove"> - + - - - - - - + + + - + + + + + android:theme="@style/AppTheme.NoActionBar"> - - + + + - - + android:parentActivityName=".ui.AccountsActivity" + android:theme="@style/AppTheme.NoActionBar" /> + android:parentActivityName=".ui.AccountsActivity"> - + + + android:label="@string/debug_info_title" + android:parentActivityName=".ui.AppSettingsActivity"> - + + @@ -124,43 +176,43 @@ android:name=".ui.TasksActivity" android:label="@string/intro_tasks_title" android:parentActivityName=".ui.AppSettingsActivity" /> - + android:exported="true" + android:label="@string/login_title" + android:parentActivityName=".ui.AccountsActivity"> - + - - - - - - - - + + + + + + + + + + + - - - - + android:theme="@style/AppTheme.NoActionBar"> - + android:parentActivityName=".ui.AccountsActivity" /> - - - - + - + + + android:resource="@xml/account_authenticator" /> - + + + android:resource="@xml/sync_calendars" /> - + + + android:resource="@xml/sync_notes" /> - + + + android:resource="@xml/sync_opentasks" /> - + + + android:resource="@xml/sync_tasks_org" /> + - - - + + android:exported="true"> - + + android:resource="@xml/account_authenticator_address_book" /> + + android:label="@string/address_books_authority_title" /> + - + + android:resource="@xml/sync_address_books" /> - + + android:resource="@xml/sync_contacts" /> - - - + android:resource="@xml/contacts" /> + @@ -313,22 +366,20 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/eelo_account_authenticator" /> - - - + - - - - - - + - - - - - - + - - - - - + @@ -425,7 +468,6 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/account_authenticator_eelo_address_book" /> - - - + - - - + @@ -469,8 +508,7 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/google_account_authenticator" /> - - - - + - - - + @@ -510,7 +545,6 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/account_authenticator_google_address_book" /> - - - + - - - - - + @@ -568,7 +598,6 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/yahoo_account_authenticator" /> - - - - - - + @@ -623,7 +648,6 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/account_authenticator_yahoo_address_book" /> - - - - - - + + + android:exported="false" + android:grantUriPermissions="true"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/ReLoginWithOidcActivity.kt b/app/src/main/java/at/bitfire/davdroid/ReLoginWithOidcActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..607b1dcc9cd349c38f6c8ce80dfae5246d04f1c0 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ReLoginWithOidcActivity.kt @@ -0,0 +1,105 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AccountManager.KEY_ACCOUNT_NAME +import android.accounts.AccountManager.KEY_BOOLEAN_RESULT +import android.accounts.AccountManager.KEY_INTENT +import android.accounts.AccountManagerFuture +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import at.bitfire.davdroid.log.Logger + +class ReLoginWithOidcActivity : AppCompatActivity() { + + private lateinit var eAccountType: String + private lateinit var accountManager: AccountManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + eAccountType = applicationContext.getString(R.string.eelo_account_type) + accountManager = AccountManager.get(this) + } + + override fun onStart() { + super.onStart() + + val accountName = intent?.extras?.getString(KEY_ACCOUNT_NAME, null) ?: run { + finishAndRemoveTask() + return + } + + val account = getAccount(accountName) + + if (isAccountRemoved && account == null) { + loginAccount(eAccountType) + } else if (!isAccountRemoved && account != null) { + logoutAccount(account) + } else { + finishAndRemoveTask() + } + } + + private fun getAccount(accountName: String) : Account? { + val eAccounts = accountManager.getAccountsByType(eAccountType) + return eAccounts.firstOrNull { it.name == accountName } + } + + private fun logoutAccount(account: Account) + { + val callback: (AliasFuture) -> Unit = { future -> + + val success = try { + future.result.getBoolean(KEY_BOOLEAN_RESULT) + } catch (exception: Exception) { + Logger.log.warning("Cannot remove account: ${account.name} : ${exception.message}") + false + } + + if (success) { + isAccountRemoved = true + Logger.log.info("Account removed. Now will sign in again with OIDC") + loginAccount(eAccountType) + } else { + Handler(Looper.getMainLooper()).post { + finishAndRemoveTask() + } + } + } + accountManager.removeAccount(account, this, callback, null) + } + + private fun loginAccount(accountType: String) { + + val callback: (AliasFuture) -> Unit = { future -> + try { + val accountName = future.result?.getString(KEY_ACCOUNT_NAME) + if (accountName == null) { + Logger.log.warning("No account re-added") + } + } catch (exception: Exception) { + Logger.log.warning("${exception.javaClass}: can't add account: ${exception.message}") + } + + Handler(Looper.getMainLooper()).post { + finishAndRemoveTask() + } + } + + accountManager.addAccount(accountType,null, null, null, this, callback, null) + } + + companion object { + private var isAccountRemoved = false + } +} + +typealias AliasFuture = AccountManagerFuture 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 d769750815e6293becf5334a2a931129bcf60fb6..67b9e0ecf20574d06e1463bc28d75d0b2bc6b544 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,22 @@ package at.bitfire.davdroid.receiver +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 androidx.core.app.NotificationManagerCompat +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ReLoginWithOidcActivity 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 /** * There are circumstances when Android drops automatic sync of accounts and resets them @@ -22,12 +32,56 @@ class BootCompletedReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Logger.log.info("Device has been rebooted; checking sync intervals etc.") + + val eAccountType = context.getString(R.string.eelo_account_type) + val accountManager = AccountManager.get(context) + // sync intervals are checked in App.onCreate() AccountUtils.getMainAccounts(context) .forEach { - val accountSettings = AccountSettings(context, it) - accountSettings.initSync() + if (eAccountType == it.type && !isLoggedWithOpenId(it, accountManager)) { + notifySwitchToOpenId(it, context) + } else { + val accountSettings = AccountSettings(context, it) + accountSettings.initSync() + } } } -} \ No newline at end of file + private fun isLoggedWithOpenId(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 notifTag = "Switch to openID" + val notifTitle = context.getString(R.string.notification_account_title, account.name) + val notifText = context.getString(R.string.notification_switch_to_openId_text) + val notifIntent = generateLogoutIntent(account.name, context) + + val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) + .setContentTitle(notifTitle) + .setContentText(notifText) + .setSmallIcon(R.drawable.ic_info) + .setContentIntent(notifIntent) + .setAutoCancel(true) + .build() + + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.notifyIfPossible(notifTag, NOTIFY_SWITCH_TO_OPENID, notification) + } + + private fun generateLogoutIntent(accountName: String, context: Context): PendingIntent { + val reLoginIntent = Intent(context, ReLoginWithOidcActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) + } + + return PendingIntent.getActivity(context, + 0, + reLoginIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_ONE_SHOT) + } +} 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 e41544d538bb2a6f08bda820781ad4d7361c0c77..4e6f04fe5598b3aafe9c1bf16b487d5bb33f4405 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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47579443b9ea55469724ce5d64711d1afbc71c1f..e08b3c0048f040376e776c642aadedb5b5867229 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ Database corrupted All accounts have been removed locally. + Debugging Other important messages Low-priority status messages @@ -53,6 +54,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. diff --git a/build.gradle b/build.gradle index e969706ab4a9dd19c37b22e1bac029d0e9bb1092..22f24469ede8926b369f1d9947eb4ee50da827a9 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ buildscript { plugins { // see https://github.com/google/ksp/releases for version numbers id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false + id 'org.jetbrains.kotlin.android' version '1.9.10' apply false } allprojects {