diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30e04639bf227a9af7c83a4dbc1829bca94a65c0..4628744db850e390cacaf5fdb775f7e5ff45bf67 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -189,7 +189,9 @@ - + - + - + . + */ +package at.bitfire.davdroid + +import android.accounts.AccountManager +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.ui.setup.EeloAuthenticatorFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +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) + setContentView(R.layout.activity_create_account) + } + + override fun onStart() { + super.onStart() + if (removeAccount()) { + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, EeloAuthenticatorFragment()) + .commit() + } else { + Logger.log.info("Vincent: Cannot remove account") + finishAndRemoveTask() + } + } + + private fun removeAccount(): Boolean { + Logger.log.info("Vincent: try to remove account") + val account = accountManager.getAccountsByType(eAccountType).firstOrNull() + + return account?.let { + accountManager.removeAccountExplicitly(it) + } ?: false + } +} 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..474404682a323a880e244aa62e6c7f5399d6db90 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,33 @@ 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.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 +import at.bitfire.davdroid.ui.setup.LoginActivity.Companion.ACCOUNT_TYPE +import at.bitfire.davdroid.ui.setup.LoginActivity.Companion.EXTRA_USERNAME +import at.bitfire.davdroid.ui.setup.LoginActivity.Companion.USERNAME_HINT +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 +41,87 @@ 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 && !isLoggedWithOpenId(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 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 title = context.getString(R.string.notification_account_title, account.name) + val contentText = context.getString(R.string.notification_switch_to_openId_text) + val intent = generateReLoginIntent(account, context) + + val notification = NotificationUtils.newBuilder(context, CHANNEL_GENERAL) + .setContentTitle(title) + .setContentText(contentText) + .setSmallIcon(R.drawable.ic_info) + .setContentIntent(intent) + .setAutoCancel(true) + .build() + + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.notifyIfPossible(NOTIFICATION_TAG, NOTIFY_SWITCH_TO_OPENID, notification) + } + + private fun generateReLoginIntent(account: Account, context: Context): PendingIntent { + val reLoginIntent = Intent(context, ReLoginWithOidcActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(ACCOUNT_TYPE, account.type) + putExtra(EXTRA_USERNAME, account.name) + putExtra(USERNAME_HINT, account.name) + } + + return PendingIntent.getActivity(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 e41544d538bb2a6f08bda820781ad4d7361c0c77..143b8329d5ecb5e63c4ca1024bb08ac4d8a2f1ca 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 @@ -80,7 +81,6 @@ object NotificationUtils { NotificationCompat.Builder(context, channel) .setColor(ResourcesCompat.getColor(context.resources, R.color.primaryColor, null)) - fun NotificationManagerCompat.notifyIfPossible(tag: String?, id: Int, notification: Notification) { try { notify(tag, id, notification) @@ -92,4 +92,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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47579443b9ea55469724ce5d64711d1afbc71c1f..60cb62446672c2dbe8857f0b8eb350adb88cec9e 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.