diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30e04639bf227a9af7c83a4dbc1829bca94a65c0..ae77090bf1f6462eee0eaf2464c2ab173cd0df75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -189,8 +189,10 @@ - + @@ -341,7 +343,7 @@ android:name="android.content.SyncAdapter" android:resource="@xml/eelo_sync_tasks" /> - + - + . + */ +package at.bitfire.davdroid + +import android.accounts.AccountManager +import android.accounts.AccountManagerFuture +import android.app.PendingIntent +import android.app.Service +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 at.bitfire.davdroid.log.Logger +class ReLoginWithOidcService: Service() { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + 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) + } else { + stopSelf() + } + + return START_STICKY + } + + 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) + + if (intent == null) { + stopSelf() + return@addAccount + } + + intent.apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + + val pendingIntent = PendingIntent.getActivity(context, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + + try { + pendingIntent.send() + } catch (exception: Exception){ + Logger.log.warning("ReLogin: Can't start login activity: ${exception.message}") + } + + stopSelf() + }, + 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? { + return 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 d769750815e6293becf5334a2a931129bcf60fb6..86e6fa1eba3889af7bb8e128b370b21558bd4d93 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.ReLoginWithOidcService 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,82 @@ 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(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(context: Context): PendingIntent { + val reLoginIntent = Intent(context, ReLoginWithOidcService::class.java) + + 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 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.