diff --git a/app/build.gradle b/app/build.gradle index 782eb21df4130782e8cc74c82e159e40cef0374a..494ccaa30105e518b39db9c53cb6f79a8cfab0ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,7 +27,15 @@ android { localProps.load(new FileInputStream(localPropsFile)) } - def appVersionCode = localProps.getProperty('VERSION_CODE', '403090007').toInteger() + 1 + def appVersionCode = localProps.getProperty('VERSION_CODE') + if (appVersionCode == null) { + // Set initial version code if not present + appVersionCode = 403090008 + } else { + // Increment version code for subsequent builds + appVersionCode = appVersionCode.toInteger() + 1 + } + localProps.setProperty('VERSION_CODE', appVersionCode.toString()) localProps.store(new FileOutputStream(localPropsFile), null) @@ -35,7 +43,7 @@ android { applicationId "foundation.e.accountmanager" versionCode appVersionCode - versionName '4.3.9-7' + versionName '4.3.9-8' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" diff --git a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt index b231268dec4dd0504bb157455b0461b1be0d7bd5..f1a481ba74b0e00194858b56eb2977cdd42f7e96 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt @@ -34,4 +34,6 @@ object Constants { const val E_SYNC_URL = "e.email" const val MURENA_DAV_URL = "https://murena.io/remote.php/dav" + + const val HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429 } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Credentials.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Credentials.kt index 4e0469f4a715112b4675c2771fbbc1726e9faa28..34b54902e80ae2081c270991f9b894b7eb00b5e0 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/Credentials.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Credentials.kt @@ -13,7 +13,8 @@ data class Credentials( val authState: AuthState? = null, val certificateAlias: String? = null, val serverUri: URI? = null, - val clientSecret: String? = null + val clientSecret: String? = null, + var passwordNeedsUpdate: Boolean = false ) { override fun toString(): String { 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 abbb5855ee3323f29a50e8f5e8db953477e002e6..6e9f23c4eae31ab3c1795f128d6404c39f11582c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -73,6 +73,7 @@ class AccountSettings( const val KEY_AUTH_STATE = "auth_state" const val KEY_CLIENT_SECRET = "client_secret" const val KEY_CERTIFICATE_ALIAS = "certificate_alias" + const val KEY_PASSWORD_NEEDS_UPDATE = "password_needs_update" const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs @@ -156,6 +157,9 @@ class AccountSettings( if (credentials.clientSecret != null) { bundle.putString(KEY_CLIENT_SECRET, credentials.clientSecret) } + + bundle.putString(KEY_PASSWORD_NEEDS_UPDATE, + credentials.passwordNeedsUpdate.toString()) } if (!cookies.isNullOrEmpty()) { @@ -270,7 +274,9 @@ class AccountSettings( accountManager.getUserData(account, KEY_USERNAME), accountManager.getPassword(account), null, - accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS), + passwordNeedsUpdate = accountManager.getUserData( + account, KEY_PASSWORD_NEEDS_UPDATE).toBoolean() ) } else { Credentials( @@ -287,6 +293,9 @@ class AccountSettings( // Basic/Digest auth accountManager.setAndVerifyUserData(account, KEY_USERNAME, credentials.userName) accountManager.setPassword(account, credentials.password) + accountManager.setAndVerifyUserData(account, KEY_PASSWORD_NEEDS_UPDATE, + credentials.passwordNeedsUpdate.toString() + ) // client certificate accountManager.setAndVerifyUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) 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 ae3b479bb536e8fb19ad9d56f7d7aa04cee9baa8..4749f57ac0e598f551e969d75f27450869f91074 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt @@ -10,12 +10,16 @@ import android.content.ContentUris import android.content.Context import android.content.Intent import android.content.SyncResult +import android.content.pm.PackageManager import android.net.Uri import android.os.RemoteException import android.provider.CalendarContract import android.provider.ContactsContract import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.edit +import androidx.core.content.pm.PackageInfoCompat +import androidx.preference.PreferenceManager import at.bitfire.dav4jvm.* import at.bitfire.dav4jvm.exception.* import at.bitfire.dav4jvm.property.GetCTag @@ -175,12 +179,20 @@ abstract class SyncManager, out CollectionType: L val workDispatcher = getWorkDispatcher() + private val PREF_LAST_VERSION_CODE = "LAST_VERSION_CODE" + /** * Call performSync with default retry values */ fun performSync() { - val authState = accountSettings.credentials().authState + val credentials = accountSettings.credentials() + if (credentials.passwordNeedsUpdate) { + val exception = UnauthorizedException(context.getString(R.string.sync_error_authentication_failed)) + notifyException(exception, null, null) + return + } + val authState = credentials.authState if (authState == null || !authState.needsTokenRefresh) { performSync(DEFAULT_RETRY_AFTER, DEFAULT_SECOND_RETRY_AFTER, DEFAULT_MAX_RETRY_TIME) return @@ -189,6 +201,34 @@ abstract class SyncManager, out CollectionType: L refreshAuthTokenAndSync(authState) } + private fun packageChanged(): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val currentVersion = getCurrentVersionCode() + val savedVersion = prefs.getLong(PREF_LAST_VERSION_CODE, 0L) + if (savedVersion != currentVersion) { + Logger.log.warning("App has been updated!" + + "Previous: $savedVersion, Current: $currentVersion") + prefs.edit { + putLong(PREF_LAST_VERSION_CODE, currentVersion) + } + // Clear cookie since the app is updated. + Logger.log.warning("Clear cookies for ${accountSettings.credentials().userName}") + accountSettings.clearCookie() + return true + } + + return false + } + + private fun getCurrentVersionCode(): Long { + return try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + PackageInfoCompat.getLongVersionCode(packageInfo) + } catch (e: PackageManager.NameNotFoundException) { + 0 + } + } + private fun refreshAuthTokenAndSync(authState: AuthState) { val tokenRequest = authState.createTokenRefreshRequest() val clientSecretString = accountSettings.credentials().clientSecret @@ -226,8 +266,7 @@ abstract class SyncManager, out CollectionType: L * @param secondRetryAfter optional param, in seconds. Used to calculate fibonnacci sequence for rety on unhandled exception * @param maxRetryTime optional param, in seconds. On unhandled exception, max time the method should retry. */ - fun performSync(retryAfter: Int, secondRetryAfter: Int, maxRetryTime: Int) { - + private fun performSync(retryAfter: Int, secondRetryAfter: Int, maxRetryTime: Int) { // dismiss previous error notifications notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR) @@ -383,6 +422,14 @@ abstract class SyncManager, out CollectionType: L is UnauthorizedException -> { Logger.log.log(Level.WARNING, "Got 401 Unauthorized", e) + if (packageChanged()) { + return@unwrapExceptions + } + val credentials = accountSettings.credentials() + if (credentials.authState == null) { + credentials.passwordNeedsUpdate = true + accountSettings.credentials(credentials) + } notifyException(e, local, remote) return@unwrapExceptions } @@ -411,6 +458,10 @@ abstract class SyncManager, out CollectionType: L if (retryAfter > 0 && secondRetryAfter > 0 && retryAfter <= maxRetryTime) { try { Logger.log.severe("Faced unhandled exception $e, Will retry sync") + if (e is HttpException && e.code == Constants.HTTP_STATUS_CODE_TOO_MANY_REQUESTS) { + Logger.log.info("HTTP 429 Too Many Requests: Retry sync cancelled") + return false + } Logger.log.info("Retry sync after $retryAfter seconds") Thread.sleep(retryAfter * 1000L) performSync(secondRetryAfter, retryAfter + secondRetryAfter, maxRetryTime) @@ -944,7 +995,6 @@ abstract class SyncManager, out CollectionType: L .setCategory(NotificationCompat.CATEGORY_ERROR) viewItemAction?.let { builder.addAction(it) } - notificationManager.notifyIfPossible(tag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build()) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt index 22cc425c293fb7e553cdbe795eb2df7c5a42d2e8..f1946ff19f2263474cb56d3d1973ed14cfd5568b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt @@ -8,6 +8,7 @@ import android.accounts.Account import android.accounts.AccountManager import android.annotation.SuppressLint import android.app.Application +import android.app.NotificationManager import android.content.Intent import android.os.Build import android.os.Bundle @@ -39,6 +40,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.syncadapter.Syncer import at.bitfire.davdroid.ui.UiUtils @@ -133,6 +135,13 @@ class SettingsActivity: AppCompatActivity() { return true } + private fun cancelNotifications() { + context?.getSystemService(NotificationManager::class.java)?.let { notificationManager -> + notificationManager.cancelAll() + Logger.log.info("All notifications canceled successfully") + } ?: Logger.log.warning("Failed to cancel notifications: Context is null") + } + private fun initSettings() { // preference group: sync findPreference(getString(R.string.settings_sync_interval_contacts_key))!!.let { @@ -292,7 +301,12 @@ class SettingsActivity: AppCompatActivity() { certificateAlias = credentials.certificateAlias, authState = credentials.authState, clientSecret = credentials.clientSecret, + passwordNeedsUpdate = false, serverUri = credentials.serverUri)) + // Cancel all notifications on password change. + cancelNotifications() + // Sync accounts once password is changed. + context?.let { SyncUtils.syncAllAccounts(it) } false } } else 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 9b05b2e607c7b685007da8fa8d3b8e480db0fa75..6e785ffc6ebfd7511ddb5656c30e209151135e61 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 = true + const val ENABLE_OIDC_SUPPORT = false } private var initialized = false