diff --git a/app/build.gradle b/app/build.gradle index 80d92b89f48404230a2a4d93cd926c8972c12e41..b51c845a098bbbb738ced698ef4f909d5280a75c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -224,7 +224,7 @@ dependencies { implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'com.google.code.gson:gson:2.10.1' - implementation("foundation.e:Nextcloud-Android-Library:1.0.8-u2.17-release") { + implementation("foundation.e:Nextcloud-Android-Library:1.0.9-u2.17-release") { exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm' exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version exclude group: 'com.squareup.okhttp3' diff --git a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java index 7d572d3b7fdce052be361056703562ec190ce14c..33c708f7bbae600d0b123c60706d3eec5e761782 100644 --- a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java +++ b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java @@ -335,7 +335,6 @@ public class InputStreamBinder extends IInputStreamService.Stub { new IllegalStateException("URL need to start with a /")); } - OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context); final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context); @@ -405,10 +404,6 @@ public class InputStreamBinder extends IInputStreamService.Stub { return path.contains("/index.php/apps/"); } - private static String getUserAgent() { - return "AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")"; - } - private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream) throws UnsupportedOperationException, com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException, @@ -429,7 +424,6 @@ public class InputStreamBinder extends IInputStreamService.Stub { new IllegalStateException("URL need to start with a /")); } - OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context); final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context); diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index ac47042b323d3fd57bec0fc6fad9f7abdda00bee..b5fff9d5f4d9fb22c97e836c11e0bd55dbab44c1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -19,6 +19,7 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -325,12 +326,7 @@ class HttpClient private constructor( object UserAgentInterceptor: Interceptor { - - // use Locale.ROOT because numbers may be encoded as non-ASCII characters in other locales - private val userAgentDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.ROOT) - private val userAgentDate = userAgentDateFormat.format(Date(BuildConfig.buildTime)) - val userAgent = "${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4jvm; " + - "okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}" + val userAgent = OwnCloudClientManagerFactory.getNextCloudUserAgent() init { Logger.log.info("Will set \"User-Agent: $userAgent\" for further requests") 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 bab2fd4ad333cfa8d0eddfa78e068bb47ae23bd7..b1254f60707013478410ae91d525286f1b54f3ae 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -635,4 +635,20 @@ class AccountSettings( Constants.DEFAULT_CONTACTS_SYNC_INTERVAL else -> Constants.DEFAULT_CALENDAR_SYNC_INTERVAL } + + fun containsPersistentCookie(): Boolean { + return !accountManager.getUserData( + account, + NCAccountUtils.Constants.KEY_OKHTTP_COOKIES + ).isNullOrBlank() + } + + fun clearCookie() { + accountManager.setUserData(account, COOKIE_KEY, null) + accountManager.setUserData( + account, + NCAccountUtils.Constants.KEY_COOKIES, + null + ) + } } 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 503ae7fc40ab48c509d5ba7a0e796359f02d886c..2a6688353d9880c0e1cdb2f9922cf37023b4f303 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncManager.kt @@ -851,9 +851,15 @@ abstract class SyncManager, out CollectionType: L syncResult.stats.numIoExceptions++ } is UnauthorizedException -> { - Logger.log.log(Level.SEVERE, "Not authorized anymore", e) 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()) { + Logger.log.log(Level.FINE, "Authorization error. Session outDated") + return + } + if (account.type.toLowerCase(Locale.getDefault()).contains("google")) { /* TODO Investigate deeper why this exception sometimes happens * https://gitlab.e.foundation/e/backlog/-/issues/3430 @@ -861,6 +867,8 @@ abstract class SyncManager, out CollectionType: L Logger.log.log(Level.WARNING, "Authorization error. Do not notify the user") return } + + Logger.log.log(Level.SEVERE, "Not authorized anymore", e) } is HttpException, is DavException -> { Logger.log.log(Level.SEVERE, "HTTP/DAV exception", e) 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 6e62c8c7d3c10f78fdde43eb44131d8efe2032b8..50f1fd84282e60dc1a16aa3f143f08d51630d47a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -292,7 +292,8 @@ class SyncWorker @AssistedInject constructor( val authority = inputData.getString(ARG_AUTHORITY) ?: throw IllegalArgumentException("$ARG_AUTHORITY required") // Check internet connection - val ignoreVpns = AccountSettings(applicationContext, account).getIgnoreVpns() + val accountSettings = AccountSettings(applicationContext, account) + val ignoreVpns = accountSettings.getIgnoreVpns() val connectivityManager = applicationContext.getSystemService()!! if (!internetAvailable(connectivityManager, ignoreVpns)) { Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.") @@ -359,6 +360,8 @@ class SyncWorker @AssistedInject constructor( // Start syncing. We still use the sync adapter framework's SyncResult to pass the sync results, but this // is only for legacy reasons and can be replaced by an own result class in the future. val result = SyncResult() + val isCookiePresent = accountSettings.containsPersistentCookie() + try { syncThread = Thread.currentThread() syncer.onPerformSync(account, extras.toTypedArray(), authority, provider, result) @@ -377,6 +380,12 @@ class SyncWorker @AssistedInject constructor( val softErrorNotificationTag = account.type + "-" + account.name + "-" + authority + if (isCookiePresent && result.stats.numAuthExceptions > 0) { + // probably the session is outDated. retry without the sessionCookie + accountSettings.clearCookie() + return Result.retry() + } + // On soft errors the sync is retried a few times before considered failed if (result.hasSoftError()) { Logger.log.warning("Soft error while syncing: result=$result, stats=${result.stats}") 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 aaf960f15c6023312f640b6424e047116af0de55..ca0ada5a1cbe89b82816e185f9e2b8ec977e2de0 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 @@ -25,7 +25,12 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.preference.* +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroup +import androidx.preference.SwitchPreferenceCompat import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials @@ -36,11 +41,11 @@ import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.syncadapter.Syncer import at.bitfire.davdroid.ui.UiUtils -import at.bitfire.davdroid.ui.setup.GoogleLoginFragment import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import com.google.android.material.snackbar.Snackbar +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -553,9 +558,21 @@ class SettingsActivity: AppCompatActivity() { fun updateCredentials(credentials: Credentials) { accountSettings?.credentials(credentials) + clearOwnCloudData() reload() } + private fun clearOwnCloudData() { + if (accountSettings?.account == null) { + return + } + + val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton() + ownCloudClientManager.removeClientForByName(accountSettings?.account?.name) + + accountSettings?.clearCookie() + } + fun updateTimeRangePastDays(days: Int?) { accountSettings?.setTimeRangePastDays(days) reload()