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 f13aa8f6c55f359952743847c173b21876e5861f..a3fb1d3ad071f2e31b44392da3f10cb37644afbc 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -124,6 +124,8 @@ class AccountSettings( const val COOKIE_KEY = NCAccountUtils.Constants.KEY_OKHTTP_COOKIES const val COOKIE_SEPARATOR = NCAccountUtils.Constants.OKHTTP_COOKIE_SEPARATOR + const val AUTH_EXCEPTION_DETECTED = "auth_exception_detected" + /** Static property to indicate whether AccountSettings migration is currently running. * **Access must be `synchronized` with `AccountSettings::class.java`.** */ @Volatile @@ -647,6 +649,21 @@ class AccountSettings( ).isNullOrBlank() } + fun noAuthExceptionDetected(): Boolean { + return accountManager.getUserData( + account, + AUTH_EXCEPTION_DETECTED + ).isNullOrBlank() + } + + fun updateAuthExceptionDetectedStatus(detected: Boolean) { + accountManager.setUserData( + account, + AUTH_EXCEPTION_DETECTED, + if (detected) true.toString() else null + ) + } + fun clearCookie() { accountManager.setUserData(account, COOKIE_KEY, null) accountManager.setUserData( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt index 2a6688353d9880c0e1cdb2f9922cf37023b4f303..c370730a921b337b08c59fd58d936be33461215d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt @@ -96,7 +96,7 @@ abstract class SyncManager, out CollectionType: L companion object { const val DEBUG_INFO_MAX_RESOURCE_DUMP_SIZE = 100*FileUtils.ONE_KB.toInt() - + const val UNAUTHORIZED_NOTIFICATION_TAG = "Unauthorized" const val MAX_MULTIGET_RESOURCES = 10 const val DEFAULT_RETRY_AFTER = 5 @@ -207,16 +207,6 @@ abstract class SyncManager, out CollectionType: L ) ) - accountSettings.credentials( - Credentials( - account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - executor.execute { performSync(DEFAULT_RETRY_AFTER, DEFAULT_SECOND_RETRY_AFTER, DEFAULT_MAX_RETRY_TIME) } @@ -233,6 +223,7 @@ abstract class SyncManager, out CollectionType: L * @param maxRetryTime optional param, in seconds. On unhandled exception, max time the method should retry. */ fun performSync(retryAfter: Int, secondRetryAfter: Int, maxRetryTime: Int) { + // dismiss previous error notifications notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR) @@ -354,6 +345,7 @@ abstract class SyncManager, out CollectionType: L else Logger.log.info("Remote collection didn't change, no reason to sync") }, { e, local, remote -> + Logger.log.info("SyncManager exception: $e") when (e) { // sync was cancelled or account has been removed: re-throw to SyncAdapterService (now BaseSyncer) is InterruptedException, @@ -385,6 +377,12 @@ abstract class SyncManager, out CollectionType: L return@unwrapExceptions } + is UnauthorizedException -> { + Logger.log.log(Level.WARNING, "Got 401 Unauthorized", e) + notifyException(e, local, remote) + return@unwrapExceptions + } + // all others else -> { // sometimes sync is kicked in when no network is not available. @@ -842,6 +840,8 @@ abstract class SyncManager, out CollectionType: L private fun notifyException(e: Throwable, local: ResourceType?, remote: HttpUrl?) { val message: String + var title: String = localCollection.title + var tag: String = notificationTag when (e) { is IOException, @@ -851,11 +851,13 @@ abstract class SyncManager, out CollectionType: L syncResult.stats.numIoExceptions++ } is UnauthorizedException -> { + title = context.getString(R.string.sync_error_authentification_failed_title, accountSettings.credentials().userName) + tag = UNAUTHORIZED_NOTIFICATION_TAG message = context.getString(R.string.sync_error_authentication_failed) syncResult.stats.numAuthExceptions++ // persistent session cookie is present. Probably the session is outDated. no need to show the notification - if (accountSettings.containsPersistentCookie()) { + if (accountSettings.containsPersistentCookie() && accountSettings.noAuthExceptionDetected()) { Logger.log.log(Level.FINE, "Authorization error. Session outDated") return } @@ -887,6 +889,8 @@ abstract class SyncManager, out CollectionType: L } } + Logger.log.info("Notification sent") + val contentIntent: Intent var viewItemAction: NotificationCompat.Action? = null @@ -920,7 +924,7 @@ abstract class SyncManager, out CollectionType: L val builder = NotificationUtils.newBuilder(context, channel) builder .setSmallIcon(R.drawable.ic_sync_problem_notify) - .setContentTitle(localCollection.title) + .setContentTitle(title) .setContentText(message) .setStyle(NotificationCompat.BigTextStyle(builder).bigText(message)) .setSubText(mainAccount.name) @@ -930,7 +934,8 @@ abstract class SyncManager, out CollectionType: L .setCategory(NotificationCompat.CATEGORY_ERROR) viewItemAction?.let { builder.addAction(it) } - notificationManager.notifyIfPossible(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build()) + + notificationManager.notifyIfPossible(tag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build()) } private fun buildDebugInfoIntent(e: Throwable, local: ResourceType?, remote: HttpUrl?) = diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt index 50f1fd84282e60dc1a16aa3f143f08d51630d47a..ab2b5a9a17238d7ce9935e536ca1c21073976878 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -363,6 +363,7 @@ class SyncWorker @AssistedInject constructor( val isCookiePresent = accountSettings.containsPersistentCookie() try { + Logger.log.log(Level.INFO, "Starting onPerformSync for $account") syncThread = Thread.currentThread() syncer.onPerformSync(account, extras.toTypedArray(), authority, provider, result) } catch (e: SecurityException) { @@ -383,6 +384,7 @@ class SyncWorker @AssistedInject constructor( if (isCookiePresent && result.stats.numAuthExceptions > 0) { // probably the session is outDated. retry without the sessionCookie accountSettings.clearCookie() + accountSettings.updateAuthExceptionDetectedStatus(detected = true) return Result.retry() } @@ -435,6 +437,8 @@ class SyncWorker @AssistedInject constructor( } } + accountSettings.updateAuthExceptionDetectedStatus(detected = false) + Logger.log.info("Sync completed successfully") return Result.success() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f0de73467f5bceeec749e8b747d9f01bc84fb65..47579443b9ea55469724ce5d64711d1afbc71c1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -538,7 +538,8 @@ Additional permissions required %s too old Minimum required version: %1$s - Authentication failed (check login credentials) + Your account %1$s + Your login/password is incorrect, please login again. Network or I/O error – %s HTTP server error – %s Local storage error – %s