From 4ce33e949b6abb5c1b824d95a3892301b6d6036b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 15 Aug 2023 17:21:24 +0200 Subject: [PATCH 01/14] Use AccountManager.setAndVerifyUserData extension method, instead of setUserData (bitfireAT/davx5#344) Hopefully fixes bitfireAT/davx5#308 * Add new setAndVerifyUserData extension function to AccountManager * Use new setAndVerifyUserData extension function instead of insecure setUserData * Update KDoc [skip CI] --------- Co-authored-by: Ricki Hirner --- .../davdroid/resource/LocalAddressBook.kt | 9 ++++--- .../davdroid/settings/AccountSettings.kt | 27 ++++++++++--------- .../settings/AccountSettingsMigrations.kt | 9 ++++--- .../davdroid/syncadapter/AccountUtils.kt | 3 ++- .../davdroid/syncadapter/ContactSyncer.kt | 3 ++- .../at/bitfire/davdroid/util/CompatUtils.kt | 21 +++++++++++++++ 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt index 3f7d5adef..e2290d12a 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -21,6 +21,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.vcard4android.* import java.io.ByteArrayOutputStream import java.util.* @@ -170,8 +171,8 @@ open class LocalAddressBook( } set(newMainAccount) { AccountManager.get(context).let { accountManager -> - accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name) - accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type) + accountManager.setAndVerifyUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name) + accountManager.setAndVerifyUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type) } _mainAccount = newMainAccount @@ -180,11 +181,11 @@ open class LocalAddressBook( var url: String get() = AccountManager.get(context).getUserData(account, USER_DATA_URL) ?: throw IllegalStateException("Address book has no URL") - set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url) + set(url) = AccountManager.get(context).setAndVerifyUserData(account, USER_DATA_URL, url) override var readOnly: Boolean get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null - set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null) + set(readOnly) = AccountManager.get(context).setAndVerifyUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null) override var lastSyncState: SyncState? get() = syncState?.let { SyncState.fromString(String(it)) } diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 757e21347..9cb6934e9 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -17,6 +17,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils +import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import dagger.hilt.EntryPoint @@ -194,14 +195,14 @@ class AccountSettings( fun credentials(credentials: Credentials) { // Basic/Digest auth - accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setAndVerifyUserData(account, KEY_USERNAME, credentials.userName) accountManager.setPassword(account, credentials.password) // client certificate - accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + accountManager.setAndVerifyUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) // OAuth - accountManager.setUserData(account, KEY_AUTH_STATE, credentials.authState?.jsonSerializeString()) + accountManager.setAndVerifyUserData(account, KEY_AUTH_STATE, credentials.authState?.jsonSerializeString()) } @@ -261,7 +262,7 @@ class AccountSettings( else -> throw IllegalArgumentException("Sync interval not applicable to authority $authority") } - accountManager.setUserData(account, key, seconds.toString()) + accountManager.setAndVerifyUserData(account, key, seconds.toString()) // update sync workers (needs already updated sync interval in AccountSettings) updatePeriodicSyncWorker(authority, seconds, getSyncWifiOnly()) @@ -323,7 +324,7 @@ class AccountSettings( accountManager.getUserData(account, KEY_WIFI_ONLY) != null fun setSyncWiFiOnly(wiFiOnly: Boolean) { - accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null) + accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null) // update sync workers (needs already updated wifi-only flag in AccountSettings) for (authority in SyncUtils.syncAuthorities(context)) @@ -340,7 +341,7 @@ class AccountSettings( } else null fun setSyncWifiOnlySSIDs(ssids: List?) = - accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(","))) + accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(","))) /** * Updates the periodic sync worker of an authority according to @@ -382,7 +383,7 @@ class AccountSettings( } fun setTimeRangePastDays(days: Int?) = - accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString()) + accountManager.setAndVerifyUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString()) /** * Takes the default alarm setting (in this order) from @@ -407,7 +408,7 @@ class AccountSettings( * start of every non-full-day event without reminder. *null*: No default reminders shall be created. */ fun setDefaultAlarm(minBefore: Int?) = - accountManager.setUserData(account, KEY_DEFAULT_ALARM, + accountManager.setAndVerifyUserData(account, KEY_DEFAULT_ALARM, if (minBefore == settings.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }) null else @@ -418,14 +419,14 @@ class AccountSettings( else accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null fun setManageCalendarColors(manage: Boolean) = - accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0") + accountManager.setAndVerifyUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0") fun getEventColors() = if (settings.containsKey(KEY_EVENT_COLORS)) settings.getBoolean(KEY_EVENT_COLORS) else accountManager.getUserData(account, KEY_EVENT_COLORS) != null fun setEventColors(useColors: Boolean) = - accountManager.setUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null) + accountManager.setAndVerifyUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null) // CardDAV settings @@ -442,7 +443,7 @@ class AccountSettings( } fun setGroupMethod(method: GroupMethod) { - accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, method.name) + accountManager.setAndVerifyUserData(account, KEY_CONTACT_GROUP_METHOD, method.name) } @@ -464,7 +465,7 @@ class AccountSettings( } fun setShowOnlyPersonal(showOnlyPersonal: Boolean) { - accountManager.setUserData(account, KEY_SHOW_ONLY_PERSONAL, if (showOnlyPersonal) "1" else null) + accountManager.setAndVerifyUserData(account, KEY_SHOW_ONLY_PERSONAL, if (showOnlyPersonal) "1" else null) } @@ -487,7 +488,7 @@ class AccountSettings( updateProc.invoke(migrations) Logger.log.info("Account version update successful") - accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString()) + accountManager.setAndVerifyUserData(account, KEY_SETTINGS_VERSION, toVersion.toString()) } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't update account settings", e) } diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt index f525f3034..25ec7ed94 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt @@ -25,6 +25,7 @@ import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.closeCompat +import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidEvent import at.bitfire.ical4android.TaskProvider @@ -229,7 +230,7 @@ class AccountSettingsMigrations( TaskUtils.currentProvider(context)?.let { provider -> val interval = accountSettings.getSyncInterval(provider.authority) if (interval != null) - accountManager.setUserData(account, + accountManager.setAndVerifyUserData(account, AccountSettings.KEY_SYNC_INTERVAL_TASKS, interval.toString()) } } @@ -318,8 +319,8 @@ class AccountSettingsMigrations( // update allowed WiFi settings key val onlySSID = accountManager.getUserData(account, "wifi_only_ssid") - accountManager.setUserData(account, AccountSettings.KEY_WIFI_ONLY_SSIDS, onlySSID) - accountManager.setUserData(account, "wifi_only_ssid", null) + accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WIFI_ONLY_SSIDS, onlySSID) + accountManager.setAndVerifyUserData(account, "wifi_only_ssid", null) } @Suppress("unused") @@ -381,7 +382,7 @@ class AccountSettingsMigrations( } // update version number so that further syncs don't repeat the migration - accountManager.setUserData(account, AccountSettings.KEY_SETTINGS_VERSION, "6") + accountManager.setAndVerifyUserData(account, AccountSettings.KEY_SETTINGS_VERSION, "6") // request sync of new address book account ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt index add388be2..26d3a3fe7 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -9,6 +9,7 @@ import android.accounts.AccountManager import android.content.Context import android.os.Bundle import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.util.setAndVerifyUserData object AccountUtils { @@ -42,7 +43,7 @@ object AccountUtils { // https://forums.bitfire.at/post/11644 if (!verifyUserData(context, account, userData)) for (key in userData.keySet()) - manager.setUserData(account, key, userData.getString(key)) + manager.setAndVerifyUserData(account, key, userData.getString(key)) if (!verifyUserData(context, account, userData)) throw IllegalStateException("Android doesn't store user data in account") diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactSyncer.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactSyncer.kt index cbb33887f..567649688 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactSyncer.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactSyncer.kt @@ -13,6 +13,7 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.util.setAndVerifyUserData import java.util.logging.Level /** @@ -50,7 +51,7 @@ class ContactSyncer(context: Context): Syncer(context) { addressBook.syncState = null } } - accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod) + accountSettings.accountManager.setAndVerifyUserData(account, PREVIOUS_GROUP_METHOD, groupMethod) Logger.log.info("Synchronizing address book: ${addressBook.url}") Logger.log.info("Taking settings from: ${addressBook.mainAccount}") diff --git a/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt index 639758afc..1ff99ba94 100644 --- a/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt @@ -4,8 +4,29 @@ package at.bitfire.davdroid.util +import android.accounts.Account +import android.accounts.AccountManager import android.content.ContentProviderClient import android.os.Build +import at.bitfire.davdroid.log.Logger + +/** + * [AccountManager.setUserData] has been found to be unreliable at times. This extension function + * checks whether the user data has actually been set and retries up to ten times before failing silently. + * + * Note: In the future we want to store accounts + associated data in the database, never calling + * so this method will become obsolete then. + */ +fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: String?) { + for (i in 1..10) { + setUserData(account, key, value) + if (getUserData(account, key) == value) + return /* success */ + + Thread.sleep(100) + } + Logger.log.warning("AccountManager failed to set $account user data $key := $value") +} @Suppress("DEPRECATION") fun ContentProviderClient.closeCompat() { -- GitLab From a375a16edf2fb06a71ead91cd80213ca22205b67 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 15 Aug 2023 18:20:14 +0200 Subject: [PATCH 02/14] Added tooltip to sync collections FAB (bitfireAT/davx5#340) * Added tooltip to sync collections fab Signed-off-by: Arnau Mora * Greyscale tint for collections sync FAB * Always use TooltipCompat for FAB tooltips Closes bitfireAT/davx5#339 --------- Signed-off-by: Arnau Mora Co-authored-by: Sunik Kupfer Co-authored-by: Ricki Hirner --- .../main/java/at/bitfire/davdroid/ui/AccountsActivity.kt | 2 ++ .../at/bitfire/davdroid/ui/account/AccountActivity.kt | 1 + app/src/main/res/layout/accounts_content.xml | 3 +-- app/src/main/res/layout/activity_account.xml | 9 ++++----- app/src/main/res/layout/activity_webdav_mounts.xml | 1 + app/src/main/res/values/strings.xml | 1 - 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt index 033e0fd38..fa3c2c7d3 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -15,6 +15,7 @@ import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.TooltipCompat import androidx.core.content.getSystemService import androidx.core.view.GravityCompat import androidx.lifecycle.AndroidViewModel @@ -62,6 +63,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele binding = ActivityAccountsBinding.inflate(layoutInflater) setContentView(binding.root) + TooltipCompat.setTooltipText(binding.content.fab, binding.content.fab.contentDescription) binding.content.fab.setOnClickListener { startActivity(Intent(this, LoginActivity::class.java)) } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/AccountActivity.kt index 1feaff290..2e9c43834 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/AccountActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -94,6 +94,7 @@ class AccountActivity: AppCompatActivity() { } // "Sync now" fab + TooltipCompat.setTooltipText(binding.sync, binding.sync.contentDescription) model.networkAvailable.observe(this) { networkAvailable -> binding.sync.setOnClickListener { if (!networkAvailable) diff --git a/app/src/main/res/layout/accounts_content.xml b/app/src/main/res/layout/accounts_content.xml index ba2f4e640..0e43cb4cc 100644 --- a/app/src/main/res/layout/accounts_content.xml +++ b/app/src/main/res/layout/accounts_content.xml @@ -30,13 +30,12 @@ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/> diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index bcb421bce..2fe72482b 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -39,26 +39,25 @@ + app:layout_anchorGravity="top|center" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/activity_webdav_mounts.xml b/app/src/main/res/layout/activity_webdav_mounts.xml index a621578a5..1f6301338 100644 --- a/app/src/main/res/layout/activity_webdav_mounts.xml +++ b/app/src/main/res/layout/activity_webdav_mounts.xml @@ -52,6 +52,7 @@ android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" android:layout_gravity="right|end|bottom" + android:contentDescription="@string/webdav_add_mount_add" app:srcCompat="@drawable/ic_add_white" /> \ 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 047c3ae50..4104fa71f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,7 +239,6 @@ There are no calendar subscriptions (yet). Swipe down to refresh the list from the server. Synchronize now - Synchronize collections Account settings Rename account Unsaved local data may be dismissed. Re-synchronization is required after renaming. New account name: -- GitLab From 273deecbe49b9f0c5ae753353ad0f8a514c4c401 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 16 Aug 2023 22:25:50 +0200 Subject: [PATCH 03/14] Update dependencies --- app/build.gradle | 10 +++---- .../davdroid/resource/LocalCalendarTest.kt | 4 +-- .../davdroid/resource/LocalEventTest.kt | 2 +- .../davdroid/resource/LocalCalendar.kt | 2 +- .../bitfire/davdroid/resource/LocalEvent.kt | 2 +- .../bitfire/davdroid/ui/DebugInfoActivity.kt | 29 ++++--------------- build.gradle | 4 +-- gradle.properties | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 19 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7feacbe18..67f90616f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,7 +125,7 @@ configurations { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' implementation "com.google.dagger:hilt-android:${versions.hilt}" @@ -138,13 +138,13 @@ dependencies { implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.core:core-ktx:1.10.1' - implementation 'androidx.fragment:fragment-ktx:1.6.0' + implementation 'androidx.fragment:fragment-ktx:1.6.1' implementation 'androidx.hilt:hilt-work:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' - implementation 'androidx.paging:paging-runtime-ktx:3.1.1' - implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.paging:paging-runtime-ktx:3.2.0' + implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.work:work-runtime-ktx:2.8.1' @@ -204,7 +204,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.work:work-testing:2.8.1' androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}" - androidTestImplementation 'io.mockk:mockk-android:1.13.4' + androidTestImplementation 'io.mockk:mockk-android:1.13.7' androidTestImplementation 'junit:junit:4.13.2' testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}" diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt index af99d2c23..79a951b11 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.kt @@ -15,8 +15,8 @@ import androidx.test.platform.app.InstrumentationRegistry import at.bitfire.davdroid.InitCalendarProviderRule import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.Event -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RecurrenceId diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt index 69629f579..47e432e2f 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/LocalEventTest.kt @@ -16,7 +16,7 @@ import androidx.test.platform.app.InstrumentationRegistry import at.bitfire.davdroid.InitCalendarProviderRule import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.Event -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt index 667760b8e..9a443d8c9 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -20,7 +20,7 @@ import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidCalendarFactory import at.bitfire.ical4android.BatchOperation import at.bitfire.ical4android.util.DateUtils -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import java.util.* import java.util.logging.Level diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt index 495e7d42f..9a2a01fc4 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.kt @@ -12,7 +12,7 @@ import android.provider.CalendarContract import android.provider.CalendarContract.Events import at.bitfire.davdroid.BuildConfig import at.bitfire.ical4android.* -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import net.fortuna.ical4j.model.property.ProdId import org.apache.commons.lang3.StringUtils import java.util.* diff --git a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt index c3777c5a4..45703adea 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -8,21 +8,12 @@ import android.accounts.Account import android.accounts.AccountManager import android.app.Application import android.app.usage.UsageStatsManager -import android.content.ContentProviderClient -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent +import android.content.* import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment -import android.os.LocaleList -import android.os.PowerManager -import android.os.StatFs +import android.os.* import android.provider.CalendarContract import android.provider.ContactsContract import android.view.View @@ -35,11 +26,7 @@ import androidx.core.content.FileProvider import androidx.core.content.getSystemService import androidx.core.content.pm.PackageInfoCompat import androidx.databinding.DataBindingUtil -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import androidx.work.WorkManager import androidx.work.WorkQuery import at.bitfire.dav4jvm.exception.DavException @@ -56,9 +43,9 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncWorker +import at.bitfire.davdroid.util.closeCompat import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.ProviderName -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import at.techbee.jtx.JtxContract import com.google.android.material.snackbar.Snackbar import dagger.assisted.Assisted @@ -74,18 +61,14 @@ import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.exception.ExceptionUtils import org.dmfs.tasks.contract.TaskContract -import java.io.File -import java.io.IOError -import java.io.IOException -import java.io.StringReader -import java.io.Writer +import java.io.* import java.util.Locale import java.util.TimeZone import java.util.logging.Level import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import javax.inject.Inject -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter as asCalendarSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter as asCalendarSyncAdapter import at.bitfire.vcard4android.Utils.asSyncAdapter as asContactsSyncAdapter import at.techbee.jtx.JtxContract.asSyncAdapter as asJtxSyncAdapter diff --git a/build.gradle b/build.gradle index 94c17cd1d..4a570731b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { ext.versions = [ aboutLibraries: '10.8.3', - appIntro: '6.2.0', + appIntro: '6.3.1', composeBom: '2023.06.01', hilt: '2.47', kotlin: '1.8.22', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion @@ -19,7 +19,7 @@ buildscript { // own libraries cert4android: 'f167e84', dav4jvm: 'da94a8b', - ical4android: 'a78e72f', + ical4android: 'b682476', vcard4android: 'bd08206' ] diff --git a/gradle.properties b/gradle.properties index b1c586316..bc2da09e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,5 @@ org.gradle.unsafe.configuration-cache=true org.gradle.unsafe.configuration-cache-problems=warn # Android -android.databinding.incremental=true android.useAndroidX=true android.enableR8.fullMode=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3a0290794..a1f2792d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -- GitLab From bd7b2714d2ae3e5e27c2995810a7849526ea3e29 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 23 Aug 2023 12:47:35 +0200 Subject: [PATCH 04/14] Upgrade AGP to 8.1.1 (bitfireAT/davx5#355) * Upgrade AGP to 8.1.1 Signed-off-by: Arnau Mora * Upgrade Kotlin and dependencies Signed-off-by: Arnau Mora * Downgrade browser Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- app/build.gradle | 7 +++---- build.gradle | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 67f90616f..1ca9acbbc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,7 @@ plugins { // Android configuration android { - compileSdkVersion 33 - buildToolsVersion '33.0.2' + compileSdk 33 defaultConfig { applicationId "at.bitfire.davdroid" @@ -51,7 +50,7 @@ android { composeOptions { // Keep this in sync with Kotlin version: // https://developer.android.com/jetpack/androidx/releases/compose-kotlin - kotlinCompilerExtensionVersion = "1.4.8" + kotlinCompilerExtensionVersion = "1.5.1" } // Java namespace for our classes (not to be confused with Android package ID) @@ -113,7 +112,7 @@ ksp { } configurations { - all { + configureEach { // exclude modules which are in conflict with system libraries exclude module: "commons-logging" exclude group: "org.json", module: "json" diff --git a/build.gradle b/build.gradle index 4a570731b..c409ddfe5 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,8 @@ buildscript { appIntro: '6.3.1', composeBom: '2023.06.01', hilt: '2.47', - kotlin: '1.8.22', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion - // * com.google.devtools.ksp at the end of this file + kotlin: '1.9.0', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion + // * com.google.devtools.ksp at the end of this file okhttp: '4.11.0', room: '2.5.2', // latest Apache Commons versions that don't require Java 8 (Android 7) @@ -32,7 +32,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.dagger:hilt-android-gradle-plugin:${versions.hilt}" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${versions.aboutLibraries}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" @@ -41,7 +41,7 @@ buildscript { plugins { // see https://github.com/google/ksp/releases for version numbers - id 'com.google.devtools.ksp' version '1.8.22-1.0.11' apply false + id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false } allprojects { -- GitLab From 795ae49da462978633f7c5e531ca2d5025c796ab Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 24 Aug 2023 16:28:18 +0200 Subject: [PATCH 05/14] SyncWorker "soft error (max retries reached)" notification confusing for users (bitfireAT/davx5#353) * Sync error notification dismiss Signed-off-by: Arnau Mora * FIXME Signed-off-by: Arnau Mora * Added click intent Signed-off-by: Arnau Mora * Delayed error info Signed-off-by: Arnau Mora * Added tag for max retries Signed-off-by: Arnau Mora * Reduced priority Signed-off-by: Arnau Mora * Removed max retries tag Signed-off-by: Arnau Mora * Using account name as tag Signed-off-by: Arnau Mora * Added authority to notification tag Signed-off-by: Arnau Mora * Added account type to notification tag Signed-off-by: Arnau Mora * Changed priority to min Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- .../at/bitfire/davdroid/syncadapter/SyncWorker.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt index d36bea229..4ef3653d3 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account +import android.app.PendingIntent import android.content.ContentProviderClient import android.content.ContentResolver import android.content.Context @@ -42,6 +43,7 @@ import androidx.work.WorkerParameters import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.account.WifiPermissionsActivity @@ -360,6 +362,8 @@ class SyncWorker @AssistedInject constructor( .putString("syncResultStats", result.stats.toString()) .build() + val softErrorNotificationTag = account.type + "-" + account.name + "-" + authority + // 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}") @@ -371,6 +375,7 @@ class SyncWorker @AssistedInject constructor( Logger.log.warning("Max retries on soft errors reached ($runAttemptCount of $MAX_RUN_ATTEMPTS). Treating as failed") notificationManager.notifyIfPossible( + softErrorNotificationTag, NotificationUtils.NOTIFY_SYNC_ERROR, NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_SYNC_IO_ERRORS) .setSmallIcon(R.drawable.ic_sync_problem_notify) @@ -378,7 +383,7 @@ class SyncWorker @AssistedInject constructor( .setContentText(applicationContext.getString(R.string.sync_error_retry_limit_reached)) .setSubText(account.name) .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setPriority(NotificationCompat.PRIORITY_MIN) .setCategory(NotificationCompat.CATEGORY_ERROR) .build() ) @@ -386,6 +391,12 @@ class SyncWorker @AssistedInject constructor( return Result.failure(syncResult) } + // If no soft error found, dismiss sync error notification + notificationManager.cancel( + softErrorNotificationTag, + NotificationUtils.NOTIFY_SYNC_ERROR + ) + // On a hard error - fail with an error message // Note: SyncManager should have notified the user if (result.hasHardError()) { -- GitLab From 62cca2939a39a8a59c341180aab9144b71ce64f6 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 28 Aug 2023 10:27:24 +0200 Subject: [PATCH 06/14] Upgrade AppIntro to 7.0.0-beta02 (bitfireAT/davx5#357) Signed-off-by: Arnau Mora --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c409ddfe5..47b902330 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { ext.versions = [ aboutLibraries: '10.8.3', - appIntro: '6.3.1', + appIntro: '7.0.0-beta02', composeBom: '2023.06.01', hilt: '2.47', kotlin: '1.9.0', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion -- GitLab From 1c419cd75ca5c75c4c370e06c4662d2a112c5f70 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 31 Aug 2023 16:26:29 +0200 Subject: [PATCH 07/14] Add setting to ignore VPNs at connection detection (bitfireAT/davx5#356) * Add setting to ignore VPNs at connection detection * Minor changes - move methods to ConnectionUtils to keep SyncWorker class compact - always use "ignore VPNs" as Boolean - other minor changes * Show ignore VPNs setting only below api lvl 23 * Change strings --------- Co-authored-by: Ricki Hirner --- .../davdroid/syncadapter/SyncWorkerTest.kt | 7 +- .../davdroid/network/ConnectionUtils.kt | 74 ++++++++++++ .../davdroid/settings/AccountSettings.kt | 37 +++--- .../davdroid/settings/DefaultsProvider.kt | 3 +- .../at/bitfire/davdroid/settings/Settings.kt | 8 +- .../davdroid/syncadapter/SyncWorker.kt | 110 ++++++------------ .../davdroid/ui/account/SettingsActivity.kt | 19 +++ app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/settings_account.xml | 7 ++ 9 files changed, 179 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt diff --git a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt index a0c9ce9a1..3ab4f1573 100644 --- a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt +++ b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt @@ -18,6 +18,7 @@ import androidx.work.workDataOf import at.bitfire.davdroid.R import at.bitfire.davdroid.TestUtils.workScheduledOrRunningOrSuccessful import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.network.ConnectionUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.NotificationUtils import dagger.hilt.android.testing.HiltAndroidRule @@ -96,8 +97,9 @@ class SyncWorkerTest { val accountSettings = AccountSettings(context, account) accountSettings.setSyncWiFiOnly(true) + mockkObject(ConnectionUtils) + every { ConnectionUtils.wifiAvailable(any()) } returns true mockkObject(SyncWorker.Companion) - every { SyncWorker.Companion.wifiAvailable(any()) } returns true every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings)) @@ -108,8 +110,9 @@ class SyncWorkerTest { val accountSettings = AccountSettings(context, account) accountSettings.setSyncWiFiOnly(true) + mockkObject(ConnectionUtils) + every { ConnectionUtils.wifiAvailable(any()) } returns false mockkObject(SyncWorker.Companion) - every { SyncWorker.Companion.wifiAvailable(any()) } returns false every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true assertFalse(SyncWorker.wifiConditionsMet(context, accountSettings)) diff --git a/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt b/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt new file mode 100644 index 000000000..7fe3eaa65 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt @@ -0,0 +1,74 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import at.bitfire.davdroid.log.Logger +import java.util.logging.Level + +object ConnectionUtils { + + /** + * Checks whether we are connected to working WiFi + */ + internal fun wifiAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService()!! + connectivityManager.allNetworks.forEach { network -> + connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) + return true + } + } + return false + } + + /** + * Checks whether we are connected to the Internet. + * + * On API 26+ devices, if a VPN is used, WorkManager might start the SyncWorker without an + * internet connection (because NET_CAPABILITY_VALIDATED is always set for VPN connections). + * To prevent the start without internet access, we don't check for VPN connections by default + * (by using [NetworkCapabilities.NET_CAPABILITY_NOT_VPN]). + * + * However in special occasions (when syncing over a VPN without validated Internet on the + * underlying connection) we do not want to exclude VPNs. + * + * @param ignoreVpns *true* filters VPN connections in the Internet check; *false* allows them as valid connection + * @return whether we are connected to the Internet + */ + @RequiresApi(23) + internal fun internetAvailable(context: Context, ignoreVpns: Boolean): Boolean { + val connectivityManager = context.getSystemService()!! + return connectivityManager.allNetworks.any { network -> + Logger.log.log(Level.FINE, "Looking for validated Internet", connectivityManager.getNetworkInfo(network)) + + connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + Logger.log.fine("Missing network capability: INTERNET") + return false + } + + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + Logger.log.fine("Missing network capability: VALIDATED") + return false + } + + if (ignoreVpns) + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + Logger.log.fine("Missing network capability: NOT_VPN") + return false + } + + /* return */ true + } ?: false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 9cb6934e9..894de543b 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -70,6 +70,7 @@ class AccountSettings( 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 + const val KEY_IGNORE_VPNS = "ignore_vpns" // ignore vpns at connection detection /** Time range limitation to the past [in days]. Values: * @@ -318,10 +319,10 @@ class AccountSettings( } fun getSyncWifiOnly() = - if (settings.containsKey(KEY_WIFI_ONLY)) - settings.getBoolean(KEY_WIFI_ONLY) - else - accountManager.getUserData(account, KEY_WIFI_ONLY) != null + if (settings.containsKey(KEY_WIFI_ONLY)) + settings.getBoolean(KEY_WIFI_ONLY) + else + accountManager.getUserData(account, KEY_WIFI_ONLY) != null fun setSyncWiFiOnly(wiFiOnly: Boolean) { accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null) @@ -332,16 +333,26 @@ class AccountSettings( } fun getSyncWifiOnlySSIDs(): List? = - if (getSyncWifiOnly()) { - val strSsids = if (settings.containsKey(KEY_WIFI_ONLY_SSIDS)) - settings.getString(KEY_WIFI_ONLY_SSIDS) - else - accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS) - strSsids?.split(',') - } else - null + if (getSyncWifiOnly()) { + val strSsids = if (settings.containsKey(KEY_WIFI_ONLY_SSIDS)) + settings.getString(KEY_WIFI_ONLY_SSIDS) + else + accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS) + strSsids?.split(',') + } else + null fun setSyncWifiOnlySSIDs(ssids: List?) = - accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(","))) + accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(","))) + + fun getIgnoreVpns(): Boolean = + when (accountManager.getUserData(account, KEY_IGNORE_VPNS)) { + null -> settings.getBoolean(KEY_IGNORE_VPNS) + "0" -> false + else -> true + } + + fun setIgnoreVpns(ignoreVpns: Boolean) = + accountManager.setAndVerifyUserData(account, KEY_IGNORE_VPNS, if (ignoreVpns) "1" else "0") /** * Updates the periodic sync worker of an authority according to diff --git a/app/src/main/java/at/bitfire/davdroid/settings/DefaultsProvider.kt b/app/src/main/java/at/bitfire/davdroid/settings/DefaultsProvider.kt index 026a61cb2..3c68d7a97 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/DefaultsProvider.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/DefaultsProvider.kt @@ -20,7 +20,8 @@ class DefaultsProvider( override val booleanDefaults = mutableMapOf( Pair(Settings.DISTRUST_SYSTEM_CERTIFICATES, false), - Pair(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false) + Pair(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false), + Pair(Settings.IGNORE_VPN_NETWORK_CAPABILITY, true) ) override val intDefaults = mapOf( diff --git a/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt b/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt index 5e9734a0e..cbdfa2c63 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt @@ -22,7 +22,13 @@ object Settings { const val PROXY_PORT = "proxy_port" // Integer /** - * Default sync interval (long), in seconds. + * Whether to ignore VPNs at internet connection detection, true by default because VPN connections + * seem to include "VALIDATED" by default even without actual internet connection + */ + const val IGNORE_VPN_NETWORK_CAPABILITY = "ignore_vpns" // Boolean + + /** + * Default sync interval (Long), in seconds. * Used to initialize an account. */ const val DEFAULT_SYNC_INTERVAL = "default_sync_interval" diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt index 4ef3653d3..d4d130156 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -11,14 +11,11 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.SyncResult -import android.net.ConnectivityManager -import android.net.NetworkCapabilities import android.net.wifi.WifiManager import android.os.Build import android.provider.CalendarContract import android.provider.ContactsContract import androidx.annotation.IntDef -import androidx.annotation.RequiresApi import androidx.concurrent.futures.CallbackToFutureAdapter import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -42,6 +39,8 @@ import androidx.work.Worker import androidx.work.WorkerParameters import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.network.ConnectionUtils.internetAvailable +import at.bitfire.davdroid.network.ConnectionUtils.wifiAvailable import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils @@ -206,44 +205,7 @@ class SyncWorker @AssistedInject constructor( } - /** - * Checks whether user imposed sync conditions from settings are met: - * - Sync only on WiFi? - * - Sync only on specific WiFi (SSID)? - * - * @param accountSettings Account settings of the account to check (and is to be synced) - * @return *true* if conditions are met; *false* if not - */ - internal fun wifiConditionsMet(context: Context, accountSettings: AccountSettings): Boolean { - // May we sync without WiFi? - if (!accountSettings.getSyncWifiOnly()) - return true // yes, continue - - // WiFi required, is it available? - if (!wifiAvailable(context)) { - Logger.log.info("Not on connected WiFi, stopping") - return false - } - // If execution reaches this point, we're on a connected WiFi - - // Check whether we are connected to the correct WiFi (in case SSID was provided) - return correctWifiSsid(context, accountSettings) - } - - /** - * Checks whether we are connected to working WiFi - */ - internal fun wifiAvailable(context: Context): Boolean { - val connectivityManager = context.getSystemService()!! - connectivityManager.allNetworks.forEach { network -> - connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) - return true - } - } - return false - } + // connection checks /** * Checks whether we are connected to the correct wifi (SSID) defined by user in the @@ -279,8 +241,34 @@ class SyncWorker @AssistedInject constructor( } return true } + + /** + * Checks whether user imposed sync conditions from settings are met: + * - Sync only on WiFi? + * - Sync only on specific WiFi (SSID)? + * + * @param accountSettings Account settings of the account to check (and is to be synced) + * @return *true* if conditions are met; *false* if not + */ + internal fun wifiConditionsMet(context: Context, accountSettings: AccountSettings): Boolean { + // May we sync without WiFi? + if (!accountSettings.getSyncWifiOnly()) + return true // yes, continue + + // WiFi required, is it available? + if (!wifiAvailable(context)) { + Logger.log.info("Not on connected WiFi, stopping") + return false + } + // If execution reaches this point, we're on a connected WiFi + + // Check whether we are connected to the correct WiFi (in case SSID was provided) + return correctWifiSsid(context, accountSettings) + } + } + private val notificationManager = NotificationManagerCompat.from(applicationContext) /** thread which runs the actual sync code (can be interrupted to stop synchronization) */ @@ -288,19 +276,20 @@ class SyncWorker @AssistedInject constructor( override fun doWork(): Result { - // Check internet connection. This is especially important on API 26+ where when a VPN is used, - // WorkManager may start the SyncWorker without a working underlying Internet connection. - if (Build.VERSION.SDK_INT >= 23 && !internetAvailable(applicationContext)) { - Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.") - return Result.failure() - } - // ensure we got the required arguments val account = Account( inputData.getString(ARG_ACCOUNT_NAME) ?: throw IllegalArgumentException("$ARG_ACCOUNT_NAME required"), inputData.getString(ARG_ACCOUNT_TYPE) ?: throw IllegalArgumentException("$ARG_ACCOUNT_TYPE required") ) val authority = inputData.getString(ARG_AUTHORITY) ?: throw IllegalArgumentException("$ARG_AUTHORITY required") + + // Check internet connection + val ignoreVpns = AccountSettings(applicationContext, account).getIgnoreVpns() + if (Build.VERSION.SDK_INT >= 23 && !internetAvailable(applicationContext, ignoreVpns)) { + Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.") + return Result.failure() + } + Logger.log.info("Running sync worker: account=$account, authority=$authority") // What are we going to sync? Select syncer based on authority @@ -408,31 +397,6 @@ class SyncWorker @AssistedInject constructor( return Result.success() } - /** - * Checks whether we are connected to the internet. - * - * On API 26+ devices, when a VPN is used, WorkManager might start the SyncWorker without an - * internet connection. To prevent this we do an extra check at the start of doWork() with this - * method. - * - * Every VPN connection also has an underlying non-vpn connection, which we find with - * [NetworkCapabilities.NET_CAPABILITY_NOT_VPN] and then check if that has validated internet - * access or not, using [NetworkCapabilities.NET_CAPABILITY_VALIDATED]. - * - * @return whether we are connected to the internet - */ - @RequiresApi(23) - private fun internetAvailable(context: Context): Boolean { - val connectivityManager = context.getSystemService()!! - return connectivityManager.allNetworks.any { network -> - connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) // filter out VPNs - && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } ?: false - } - } - override fun onStopped() { Logger.log.info("Stopping sync thread") syncThread?.interrupt() diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt index 303d65367..06b60718e 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt @@ -217,6 +217,18 @@ class SettingsActivity: AppCompatActivity() { } } + findPreference(getString(R.string.settings_ignore_vpns_key))!!.let { + model.ignoreVpns.observe(viewLifecycleOwner) { ignoreVpns -> + it.isEnabled = true + it.isChecked = ignoreVpns + it.isVisible = Build.VERSION.SDK_INT >= 23 + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, prefValue -> + model.updateIgnoreVpns(prefValue as Boolean) + false + } + } + } + // preference group: authentication val prefUserName = findPreference(getString(R.string.settings_username_key))!! val prefPassword = findPreference(getString(R.string.settings_password_key))!! @@ -443,6 +455,7 @@ class SettingsActivity: AppCompatActivity() { val syncWifiOnly = MutableLiveData() val syncWifiOnlySSIDs = MutableLiveData>() + val ignoreVpns = MutableLiveData() val credentials = MutableLiveData() @@ -481,6 +494,7 @@ class SettingsActivity: AppCompatActivity() { syncWifiOnly.postValue(accountSettings.getSyncWifiOnly()) syncWifiOnlySSIDs.postValue(accountSettings.getSyncWifiOnlySSIDs()) + ignoreVpns.postValue(accountSettings.getIgnoreVpns()) credentials.postValue(accountSettings.credentials()) @@ -510,6 +524,11 @@ class SettingsActivity: AppCompatActivity() { reload() } + fun updateIgnoreVpns(ignoreVpns: Boolean) { + accountSettings?.setIgnoreVpns(ignoreVpns) + reload() + } + fun updateCredentials(credentials: Credentials) { accountSettings?.credentials(credentials) reload() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4104fa71f..3805959a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -345,6 +345,10 @@ Comma-separated names (SSIDs) of allowed WiFi networks (leave blank for all) WiFi SSID restriction requires further settings Manage + ignore_vpns + VPN connectivity + Non-VPN connection required (recommended) + VPN counts as Internet connection Authentication oauth Re-authenticate diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index 2f46d8da6..336f68b25 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -43,6 +43,13 @@ android:title="@string/settings_sync_wifi_only_ssids" android:dialogMessage="@string/settings_sync_wifi_only_ssids_message"/> + + Date: Fri, 1 Sep 2023 10:50:37 +0200 Subject: [PATCH 08/14] Update dependencies (including cert4android and vcard4android) (bitfireAT/davx5#360) * Update dependencies (including cert4android and vcard4android) * Migrated to new version of ical4android Signed-off-by: Arnau Mora * Increased compileSdk Signed-off-by: Arnau Mora * Upgraded browser Signed-off-by: Arnau Mora * Only use `appInForeground` for `customCertsUi` Signed-off-by: Arnau Mora * Removed unnecessary variable and fixed trust manager Signed-off-by: Arnau Mora * Cleaned up trust manager factory Signed-off-by: Arnau Mora * Update dependencies (including cert4android and vcard4android) * Migrated to new version of ical4android Signed-off-by: Arnau Mora * Increased compileSdk Signed-off-by: Arnau Mora * Upgraded browser Signed-off-by: Arnau Mora * Only use `appInForeground` for `customCertsUi` Signed-off-by: Arnau Mora * Removed unnecessary variable and fixed trust manager Signed-off-by: Arnau Mora * Cleaned up trust manager factory Signed-off-by: Arnau Mora * Minor changes * Fixed build for SDK 34 Signed-off-by: Arnau Mora * Migrated certificate trusting Signed-off-by: Arnau Mora * NetworkConfigProvider: handle invalid trusted certificate --------- Signed-off-by: Arnau Mora Co-authored-by: Arnau Mora --- app/build.gradle | 8 +-- .../java/at/bitfire/davdroid/log/Logger.kt | 2 +- .../at/bitfire/davdroid/network/HttpClient.kt | 54 +++++++++---------- .../settings/SharedPreferencesProvider.kt | 2 +- .../davdroid/ui/AppSettingsActivity.kt | 9 ++-- build.gradle | 7 +-- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1ca9acbbc..bc36dff3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ plugins { // Android configuration android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "at.bitfire.davdroid" @@ -132,7 +132,7 @@ dependencies { // support libs implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.browser:browser:1.5.0' + implementation 'androidx.browser:browser:1.6.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -146,7 +146,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.work:work-runtime-ktx:2.8.1' + implementation "androidx.work:work-runtime-ktx:${versions.workManager}" implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.material:material:1.9.0' @@ -201,7 +201,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' - androidTestImplementation 'androidx.work:work-testing:2.8.1' + androidTestImplementation "androidx.work:work-testing:${versions.workManager}" androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}" androidTestImplementation 'io.mockk:mockk-android:1.13.7' androidTestImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/at/bitfire/davdroid/log/Logger.kt b/app/src/main/java/at/bitfire/davdroid/log/Logger.kt index a4c3fdf40..0b0303f01 100644 --- a/app/src/main/java/at/bitfire/davdroid/log/Logger.kt +++ b/app/src/main/java/at/bitfire/davdroid/log/Logger.kt @@ -45,7 +45,7 @@ object Logger : SharedPreferences.OnSharedPreferenceChangeListener { reinitialize() } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { if (key == LOG_TO_FILE) { log.info("Logging settings changed; re-initializing logger") reinitialize() diff --git a/app/src/main/java/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/java/at/bitfire/davdroid/network/HttpClient.kt index e02676eee..42fee8dd9 100644 --- a/app/src/main/java/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/java/at/bitfire/davdroid/network/HttpClient.kt @@ -8,6 +8,7 @@ import android.content.Context import android.os.Build import android.security.KeyChain import at.bitfire.cert4android.CustomCertManager +import at.bitfire.cert4android.CustomHostnameVerifier import at.bitfire.dav4jvm.BasicDigestAuthHandler import at.bitfire.dav4jvm.UrlUtils import at.bitfire.davdroid.BuildConfig @@ -20,6 +21,7 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.MutableStateFlow import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationService import okhttp3.* @@ -40,7 +42,6 @@ import javax.net.ssl.* class HttpClient private constructor( val okHttpClient: OkHttpClient, - private val certManager: CustomCertManager? = null, private var authService: AuthorizationService? = null ): AutoCloseable { @@ -87,7 +88,6 @@ class HttpClient private constructor( override fun close() { authService?.dispose() okHttpClient.cache?.close() - certManager?.close() } @@ -102,7 +102,8 @@ class HttpClient private constructor( fun certManager(): CustomCertManager } - private var appInForeground = false + private var appInForeground: MutableStateFlow? = + MutableStateFlow(false) private var authService: AuthorizationService? = null private var certManagerProducer: CertManagerProducer? = null private var certificateAlias: String? = null @@ -151,7 +152,7 @@ class HttpClient private constructor( customCertManager { // by default, use a CustomCertManager that respects the "distrust system certificates" setting val trustSystemCerts = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES) - CustomCertManager(context, true /*BuildConfig.customCertsUI*/, trustSystemCerts) + CustomCertManager(context, trustSystemCerts, appInForeground) } // use account settings for authentication and cookies @@ -205,7 +206,7 @@ class HttpClient private constructor( certManagerProducer = producer } fun setForeground(foreground: Boolean): Builder { - appInForeground = foreground + appInForeground?.value = foreground return this } @@ -261,33 +262,32 @@ class HttpClient private constructor( orig.protocols(listOf(Protocol.HTTP_1_1)) } - val certManager = - if (certManagerProducer != null || keyManager != null) { - val manager = certManagerProducer?.certManager() - manager?.appInForeground = appInForeground + if (certManagerProducer != null || keyManager != null) { + val manager = certManagerProducer?.certManager() - val trustManager = manager ?: { // fall back to system default trust manager - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + val trustManager = manager ?: /* fall back to system default trust manager */ + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + .let { factory -> factory.init(null as KeyStore?) factory.trustManagers.first() as X509TrustManager - }() - - val hostnameVerifier = manager?.hostnameVerifier(OkHostnameVerifier) - ?: OkHostnameVerifier - - val sslContext = SSLContext.getInstance("TLS") - sslContext.init( - if (keyManager != null) arrayOf(keyManager) else null, - arrayOf(trustManager), - null) - orig.sslSocketFactory(sslContext.socketFactory, trustManager) - orig.hostnameVerifier(hostnameVerifier) + } - manager - } else - null + val hostnameVerifier = + if (manager != null) + CustomHostnameVerifier(context, OkHostnameVerifier) + else + OkHostnameVerifier + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + if (keyManager != null) arrayOf(keyManager) else null, + arrayOf(trustManager), + null) + orig.sslSocketFactory(sslContext.socketFactory, trustManager) + orig.hostnameVerifier(hostnameVerifier) + } - return HttpClient(orig.build(), certManager = certManager, authService = authService) + return HttpClient(orig.build(), authService = authService) } } diff --git a/app/src/main/java/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt b/app/src/main/java/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt index ac2d9e3b7..70d8caed9 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt @@ -52,7 +52,7 @@ class SharedPreferencesProvider( override fun canWrite() = true - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { settingsManager.onSettingsChanged() } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 9f1cd224e..9ed784ff9 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -15,9 +15,8 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.LocaleListCompat import androidx.preference.* -import at.bitfire.cert4android.CustomCertManager +import at.bitfire.cert4android.CustomCertStore import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.ForegroundService import at.bitfire.davdroid.R @@ -33,7 +32,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.net.URI import java.net.URISyntaxException -import java.util.* import javax.inject.Inject import kotlin.math.roundToInt @@ -291,8 +289,9 @@ class AppSettingsActivity: AppCompatActivity() { } private fun resetCertificates() { - if (CustomCertManager.resetCertificates(requireActivity())) - Snackbar.make(requireView(), getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show() + CustomCertStore.getInstance(requireActivity()).clearUserDecisions() + + Snackbar.make(requireView(), getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show() } } diff --git a/build.gradle b/build.gradle index 47b902330..fb24a440c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,21 +6,22 @@ buildscript { ext.versions = [ aboutLibraries: '10.8.3', appIntro: '7.0.0-beta02', - composeBom: '2023.06.01', + composeBom: '2023.08.00', hilt: '2.47', kotlin: '1.9.0', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion // * com.google.devtools.ksp at the end of this file okhttp: '4.11.0', room: '2.5.2', + workManager: '2.8.1', // latest Apache Commons versions that don't require Java 8 (Android 7) commonsCollections: '4.2', commonsLang: '3.8.1', commonsText: '1.3', // own libraries - cert4android: 'f167e84', + cert4android: 'd6fd798', dav4jvm: 'da94a8b', ical4android: 'b682476', - vcard4android: 'bd08206' + vcard4android: '1665081' ] repositories { -- GitLab From 251cf1b2e90f4bd8f3e7aa73d6e8f130096e3294 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 5 Sep 2023 10:32:10 +0200 Subject: [PATCH 09/14] Update dependencies (including vcard4android), bump version to 4.3.6-rc.1 --- app/build.gradle | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bc36dff3f..c5c034160 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403050200 - versionName '4.3.5.2' + versionCode 403060001 + versionName '4.3.6-rc.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" diff --git a/build.gradle b/build.gradle index fb24a440c..ac2c34bab 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { aboutLibraries: '10.8.3', appIntro: '7.0.0-beta02', composeBom: '2023.08.00', - hilt: '2.47', + hilt: '2.48', kotlin: '1.9.0', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion // * com.google.devtools.ksp at the end of this file okhttp: '4.11.0', @@ -21,7 +21,7 @@ buildscript { cert4android: 'd6fd798', dav4jvm: 'da94a8b', ical4android: 'b682476', - vcard4android: '1665081' + vcard4android: 'b376d2e' ] repositories { -- GitLab From b670979f12ccfc1c22521545dfd82c6f721e69a4 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 5 Sep 2023 20:30:30 +0200 Subject: [PATCH 10/14] Fetch translations from Transifex --- app/src/main/assets/translators.json | 1 - app/src/main/res/values-bg/strings.xml | 42 ++++++++++++++++++-------- app/src/main/res/values-ca/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 4 ++- app/src/main/res/values-nl/strings.xml | 10 ++++++ app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 5 ++- app/src/main/res/values-zh/strings.xml | 1 - 11 files changed, 45 insertions(+), 23 deletions(-) diff --git a/app/src/main/assets/translators.json b/app/src/main/assets/translators.json index eb66b1fa1..e69de29bb 100644 --- a/app/src/main/assets/translators.json +++ b/app/src/main/assets/translators.json @@ -1 +0,0 @@ -{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","jordibrus","zagur"],"cs":["pavelb","tomas.odehnal"],"da":["Tntdruid_","knutztar","mjjzf","twikedk"],"de":["Atalanttore","TheName","Wyrrrd","YvanM","amandablue","anestiskaci","corppneq","crit12","hammaschlach","maxkl","nicolas_git","owncube"],"el":["KristinaQejvanaj","anestiskaci","diamond_gr"],"es":["Ark74","Elhea","GranPC","aluaces","jcvielma","plaguna","polkhas","xphnx"],"eu":["Osoitz","Thadah","cockeredradiation"],"fa":["Numb","ahangarha","amiraliakbari","joojoojoo","maryambehzi","mtashackori","taranehsaei"],"fr":["AlainR","Amadeen","Floflr","Llorc","LoiX07","Novick","Poussinou","Thecross","YvanM","alkino2","boutil","callmemagnus","chfo","chrcha","grenatrad","jokx","mathieugfortin","paullbn","vincen","ÉricB."],"fr_FR":["Llorc","Poussinou","chrcha"],"gl":["aluaces","pikamoku"],"hu":["Roshek","jtg"],"it":["Damtux","FranzMari","ed0","malaerba","noccio","nwandy","rickyroo","technezio"],"it_IT":["malaerba"],"ja":["Naofumi","yanorei32"],"nb_NO":["elonus"],"nl":["XtremeNova","davtemp","dehart","erikhubers","frankyboy1963","toonvangerwen"],"pl":["TORminator","TheName","Valdnet","gsz","mg6","oskarjakiela"],"pt":["amalvarenga","wanderlei.huttel"],"pt_BR":["wanderlei.huttel"],"ru":["aigoshin","anm","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"sv":["Mikaelb","campbelldavid"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy","waiabsfabuloushk"]} diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 2f7ce8f27..fa8e029b7 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -11,7 +11,8 @@ Помощ Управление на регистрации Споделяне - Повредена банка от данни + Няма връзка с интернет, насрочено е синхронизиране за по-късно + Повредено хранилище Всички местни профили са премахнати. Отстраняване на дефекти Други важни съобщения @@ -47,7 +48,7 @@ Няма достъпен магазин за приложения Не се нуждая от поддръжка на задачи.* Приложение с отворен код - Радваме се, че използвате %s. Това е приложение с отворен код. Разработката, издръжката и поддръжката му са тежка работа. Моля да допринесете (има много начини) или да дарите. Вашият жест ще бъде високо оценен. + Радваме се, че използвате %s. Това е приложение с отворен код. Разработката, издръжката и поддръжката му са тежка работа. Молим да допринесете (има много начини) или да дарите. Вашият жест ще бъде високо оценен. Как да допринеса или даря Без повторно показване в близко бъдеще @@ -188,8 +189,8 @@ Тъмна Нулиране на подсказки - Всички подсказки, които са били разкарани, ще бъдат показани отново - Всички подсказки ще се покажат отново + Всички затворени подсказки, ще бъдат показани отново + Всички подсказки ще бъдат показани отново Интеграция Приложението Tasks Синхронизиране чрез %s @@ -234,13 +235,13 @@ Нова регистрация Вход с електронна поща Електронна поща - Нужен е действителен електронен адрес + Необходим е действителен електронен адрес Парола - Трябва парола + Необходима е парола Вход с адрес и потребителско име Адресът трябва да започва с http(s):// Потребителско име - Нужно е потребителско име + Необходимо е потребителско име Основен адрес Избери сертификат Напред @@ -257,6 +258,13 @@ Със сертификат за удостоверяване Не е намерен сертификат Инсталиране на сертификат + Google Contacts / Calendar + Погледнете страницата „Проверено с Гугъл“ за последната информация. + Може да получите неочаквани предупреждения и/или да се наложи да създадете собствен клиентски инентификатор. + Профил в Гугъл + Вход с Гугъл + Идентификатор на клиент (по желание) + Не може да бъде получен код за упълномощаване Откриване на настройки Изчакайте, запитване на сървъра… Не са открити услуги на CalDAV или CardDAV. @@ -281,7 +289,7 @@ Синхронизиране само през Wi-Fi Синхронизацията е ограничена само при връзки през Wi-Fi - Вида на връзката не е от значение + Видът на връзката не е от значение Ограничения на Wi-Fi по SSID Ще синхронизира само през %s Ще синхронизира само през %s (изисква услуга за местоположението) @@ -289,11 +297,17 @@ Разделени със запетая имена (SSID) на разрешените мрежи по Wi-Fi (празно за всички мрежи) Ограниченията на Wi-Fi по SSID изискват допълнителни настройки Управление + Свързаност през ВЧМ + Необходима е свързаност извън ВЧМ (препоръчително) + ВЧМ се счита като интернет Удостоверяване + Повторно удостоверяване + Изпълнява повторно влизане чрез OAuth + потребителско име Потребителско име - Въведи потребитеско име: + Въведете потребитеско име: Парола - Промени паролата съгласно твоя сървър. + Променете паролата съгласно сървъра. Въведете парола: Псевдоним на клиентския сертификат Няма избран сертификат @@ -331,22 +345,24 @@ Възможни компоненти в календара Събития Задачи - Бележки / журнал + Бележки / дневник Цвят Създаване на ресурс Заглавие - Изисква се заглавие + Необходимо е заглавие Описание по желание Местоположение на хранилището Местоположението на хранилището е задължително Създаване Премахване - Сигурен ли си? + Сигурни ли сте? Този ресурс (%s) и всичките данни в него ще бъдат изтрити. Тези данни ще бъдат премахнати от сървъра. Само за четене Свойства + Последно синхронизирано: + Никога Адрес: Собственик diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 852c8e10e..b0f5c4813 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -262,6 +262,7 @@ Per favor, llig la nostra guia \"Tested with Google\" per a obtenir informació actualitzada És possible que sorgisca alguna advertència o error, en eixe cas hauràs de crear el teu propi ID de client. Compte de Google + Inicia la sessió amb Google ID de Client (opcional) política de privadesa per als detalls.]]> Política de dades d\'usuari dels servis de l\'API de Google, incloent els requisits d\'ús limitat.]]> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6a7686d78..25333c71a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -210,7 +210,6 @@ (Noch) keine Kalender-Abos vorhanden Nach unten wischen, um neue Einträge am Server zu suchen. Jetzt synchronisieren - Synchronisiere die Kollektionen Konto-Einstellungen Konto umbenennen Ungespeicherte lokale Änderungen können verloren gehen. Nach dem Umbenennen muss neu synchronisiert werden. Neuer Kontoname: diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index b2cdfe17c..28a69a4ef 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -210,7 +210,6 @@ Ez dago egutegi harpidetzarik (oraindik). Lerratu beherantz zerbitzariaren zerrenda freskatzeko. Sinkronizatu orain - Sinkronizatu bildumak Kontuaren ezarpenak Berrizendatu kontua Gorde gabeko datu lokalak baztertu daitezke. Berriro sinkronizatu behar da izena aldatu ostean. Kontuaren izen berria: diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index e58cf328e..93a31b5ce 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -210,7 +210,6 @@ Non tes subscricións a calendario (aínda). Arrastra hacia abaixo para actualizar a lista desde o servidor. Sincronizar agora - Sincronizar coleccións Axustes da conta Renomear conta Os datos locais non gardados perderanse. A resincronización é precisa tras renomear. Novo nome da conta: diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 0866d0138..58354eb0c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -210,7 +210,6 @@ カレンダーのサブスクリプションは (まだ) ありません。 下にスワイプすると、サーバーからリストを更新します。 今すぐ同期 - コレクションを同期 アカウント設定 アカウントの名前を変更 未保存のローカルデータが破棄されることがあります。名前の変更後に再同期が必要です。新しいアカウント名: @@ -300,6 +299,9 @@ 利用可能な WiFi ネットワークのカンマ区切りの名前 (SSID) (空白にするとすべて) WiFi SSID 制限にはさらに設定が必要です 管理 + VPN 接続 + VPN でない接続を要求 (推奨) + VPN を一般のインターネット接続として扱う 認証 再認証 もう一度 OAuth ログインを実行 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3cd5496b4..686af1896 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -259,7 +259,14 @@ Geen certificaat gevonden Certificaat installeren Google Contacten / Kalender + Raadpleeg onze pagina \"Getest met Google\" voor actuele informatie. + Het kan zijn dat je onverwachte waarschuwingen krijgt en/of je eigen client-ID moet aanmaken. Google account + Inloggen met Google + Client ID (optioneel) + Privacybeleid voor meer informatie.]]> + beleid voor gebruikersgegevens van Google API Services, met inbegrip van de vereisten voor beperkt gebruik.]]> + Kon geen autorisatiecode verkrijgen Configuratie detecteren Even geduld, verzoek naar server… Geen CalDAV- of CardDAV-service gevonden. @@ -293,6 +300,9 @@ Beperking WiFi-SSID vereist verdere instellingen Beheren Verificatie + Opnieuw authenticeren + Voer OAuth-aanmelding opnieuw uit + Gebruikersnaam Gebruikersnaam Gebruikersnaam invoeren: Wachtwoord diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 41353c7ee..b6043a64f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -210,7 +210,6 @@ Nie ma (jeszcze) subskrypcji kalendarzy. Przesuń palcem w dół, aby odświeżyć listę z serwera. Synchronizuj teraz - Synchronizuj kolekcje Ustawienia konta Zmień nazwę konta Niezapisane dane lokalne mogą zostać usunięte. Po zmianie nazwy jest wymagana ponowna synchronizacja. Nowa nazwa konta: diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8fe79b695..6a3f77a8d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -210,7 +210,6 @@ Нет подписок на календарь (пока). Потяните вниз, чтобы обновить список с сервера. Синхронизировать - Синхронизировать коллекции Настройки аккаунта Переименовать аккаунт Несохраненные локальные данные могут быть потеряны. Необходима повторная синхронизация после переименования. Новое имя аккаунта: @@ -260,12 +259,12 @@ Сертификат не найден Установить сертификат Google Контакты / Календарь - Актуальную информацию смотрите на нашей странице \" Протестировано с Google\". + Актуальную информацию смотрите на нашей странице \"Протестировано с Google\". Вы можете столкнуться с неожиданными предупреждениями и/или вам придется создать свой собственный ID клиента. Google аккаунт Войти через Google ID клиента (необязательно) - Политику конфиденциальности.]]> + Политику конфиденциальности.]]> Политику в отношении пользовательских данных Google API Services, включая требования Ограниченного использования.]]> Не удалось получить код авторизации Обнаружение конфигурации diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 4ba55d05e..bd18a464a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -210,7 +210,6 @@ 暂无日历订阅。 下拉可从服务器获取最新列表。 立即同步 - 同步收藏 账户设置 重命名账户 重命名后,未上传的本地修改会被撤销,您需要重新执行同步。新账户名: -- GitLab From 56402503592c89a50cd6f74356327c1a0b9f797b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 5 Sep 2023 16:43:30 +0200 Subject: [PATCH 11/14] Wait an appropriate delay before SyncWorker retries after soft errors (bitfireAT/davx5#337) * Use appropriate delayUntil value for retrying syncs on 503s. Crop server suggested retryAfter value to self defined min/max values and use a reasonable default value if non-existent. * Add tests for getDelayUntil * Wait appropriate delay, before retrying sync after a soft error happened * Increase max and default sync delays after soft errors * Increase initial backoff time for SyncWorker retries * Minor getDelayUntil changes * Minor changes - store delayUntil in seconds - pass duration instead of timestamp to Thread.sleep - other minor changes * Use Instant instead of Long timestamps * Correct calculation of blocking duration * Indicate soft error occurred on 503 server message --------- Co-authored-by: Ricki Hirner --- .../java/at/bitfire/davdroid/TestUtils.kt | 9 +++++ .../davdroid/syncadapter/SyncManagerTest.kt | 38 ++++++++++++++++-- .../davdroid/syncadapter/SyncManager.kt | 39 +++++++++++++++++-- .../davdroid/syncadapter/SyncWorker.kt | 13 +++++-- 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/at/bitfire/davdroid/TestUtils.kt b/app/src/androidTest/java/at/bitfire/davdroid/TestUtils.kt index 05b1e7e96..c7b9193aa 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/TestUtils.kt +++ b/app/src/androidTest/java/at/bitfire/davdroid/TestUtils.kt @@ -11,12 +11,21 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery import org.jetbrains.annotations.TestOnly +import org.junit.Assert.assertTrue import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException object TestUtils { + fun assertWithin(expected: Long, actual: Long, tolerance: Long) { + val absDifference = Math.abs(expected - actual) + assertTrue( + "$actual not within ($expected ± $tolerance)", + absDifference <= tolerance + ) + } + @TestOnly fun workScheduledOrRunning(context: Context, workerName: String): Boolean = workInStates(context, workerName, listOf( diff --git a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncManagerTest.kt b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncManagerTest.kt index 562fb5e41..5848c1e00 100644 --- a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncManagerTest.kt +++ b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncManagerTest.kt @@ -19,6 +19,7 @@ import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.Response.HrefRelation import at.bitfire.dav4jvm.property.GetETag import at.bitfire.davdroid.R +import at.bitfire.davdroid.TestUtils.assertWithin import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.network.HttpClient @@ -30,8 +31,15 @@ import okhttp3.Protocol import okhttp3.internal.http.StatusLine import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer -import org.junit.* -import org.junit.Assert.* +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test import java.time.Instant import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -112,6 +120,30 @@ class SyncManagerTest { } + @Test + fun testGetDelayUntil_defaultOnNull() { + val now = Instant.now() + val delayUntil = SyncManager.getDelayUntil(null).epochSecond + val default = now.plusSeconds(SyncManager.DELAY_UNTIL_DEFAULT).epochSecond + assertWithin(default, delayUntil, 5) + } + + @Test + fun testGetDelayUntil_reducesToMax() { + val now = Instant.now() + val delayUntil = SyncManager.getDelayUntil(now.plusSeconds(10*24*60*60)).epochSecond + val max = now.plusSeconds(SyncManager.DELAY_UNTIL_MAX).epochSecond + assertWithin(max, delayUntil, 5) + } + + @Test + fun testGetDelayUntil_increasesToMin() { + val delayUntil = SyncManager.getDelayUntil(Instant.EPOCH).epochSecond + val min = Instant.now().plusSeconds(SyncManager.DELAY_UNTIL_MIN).epochSecond + assertWithin(min, delayUntil, 5) + } + + private fun queryCapabilitiesResponse(cTag: String? = null): MockResponse { val body = StringBuilder() body.append("\n" + @@ -148,7 +180,7 @@ class SyncManagerTest { .plusSeconds(60) .toEpochMilli() // 5 sec tolerance for test - assertTrue(result.delayUntil > (expected - 5000) && result.delayUntil < (expected + 5000)) + assertWithin(expected, result.delayUntil*1000, 5000) } @Test diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt index 7a351ea73..b9695bbbc 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt @@ -54,6 +54,7 @@ import java.io.InterruptedIOException import java.lang.ref.WeakReference import java.net.HttpURLConnection import java.security.cert.CertificateException +import java.time.Instant import java.util.* import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor @@ -87,8 +88,39 @@ abstract class SyncManager, out CollectionType: L companion object { const val DEBUG_INFO_MAX_RESOURCE_DUMP_SIZE = 100*FileUtils.ONE_KB.toInt() + const val MAX_MULTIGET_RESOURCES = 10 + const val DELAY_UNTIL_DEFAULT = 15*60L // 15 min + const val DELAY_UNTIL_MIN = 1*60L // 1 min + const val DELAY_UNTIL_MAX = 2*60*60L // 2 hours + + /** + * Returns appropriate sync retry delay in seconds, considering the servers suggestion + * ([DELAY_UNTIL_DEFAULT] if no server suggestion). + * + * Takes current time into account to calculate intervals. Interval + * will be restricted to values between [DELAY_UNTIL_MIN] and [DELAY_UNTIL_MAX]. + * + * @param retryAfter optional server suggestion on how long to wait before retrying + * @return until when to wait before sync can be retried + */ + fun getDelayUntil(retryAfter: Instant?): Instant { + val now = Instant.now() + + if (retryAfter == null) + return now.plusSeconds(DELAY_UNTIL_DEFAULT) + + // take server suggestion, but restricted to plausible min/max values + val min = now.plusSeconds(DELAY_UNTIL_MIN) + val max = now.plusSeconds(DELAY_UNTIL_MAX) + return when { + min > retryAfter -> min + max < retryAfter -> max + else -> retryAfter + } + } + var _workDispatcher: WeakReference? = null /** * We use our own dispatcher to @@ -109,6 +141,7 @@ abstract class SyncManager, out CollectionType: L ).asCoroutineDispatcher() return newDispatcher } + } init { @@ -274,9 +307,9 @@ abstract class SyncManager, out CollectionType: L // specific HTTP errors is ServiceUnavailableException -> { Logger.log.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e) - e.retryAfter?.let { retryAfter -> - syncResult.delayUntil = retryAfter.toEpochMilli() - } + // determine when to retry + syncResult.delayUntil = getDelayUntil(e.retryAfter).epochSecond + syncResult.stats.numIoExceptions++ // Indicate a soft error occurred } // all others diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt index d4d130156..9ca95ec5c 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -5,7 +5,6 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account -import android.app.PendingIntent import android.content.ContentProviderClient import android.content.ContentResolver import android.content.Context @@ -42,7 +41,6 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.ConnectionUtils.internetAvailable import at.bitfire.davdroid.network.ConnectionUtils.wifiAvailable import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.account.WifiPermissionsActivity @@ -149,7 +147,7 @@ class SyncWorker @AssistedInject constructor( .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, - WorkRequest.MIN_BACKOFF_MILLIS, + WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, // 30 sec TimeUnit.MILLISECONDS ) .setConstraints(constraints) @@ -275,7 +273,6 @@ class SyncWorker @AssistedInject constructor( var syncThread: Thread? = null override fun doWork(): Result { - // ensure we got the required arguments val account = Account( inputData.getString(ARG_ACCOUNT_NAME) ?: throw IllegalArgumentException("$ARG_ACCOUNT_NAME required"), @@ -357,6 +354,14 @@ class SyncWorker @AssistedInject constructor( if (result.hasSoftError()) { Logger.log.warning("Soft error while syncing: result=$result, stats=${result.stats}") if (runAttemptCount < MAX_RUN_ATTEMPTS) { + val blockDuration = result.delayUntil - System.currentTimeMillis()/1000 + Logger.log.warning("Waiting for $blockDuration seconds, before retrying ...") + + // We block the SyncWorker here so that it won't be started by the sync framework immediately again. + // This should be replaced by proper work scheduling as soon as we don't depend on the sync framework anymore. + if (blockDuration > 0) + Thread.sleep(blockDuration*1000) + Logger.log.warning("Retrying on soft error (attempt $runAttemptCount of $MAX_RUN_ATTEMPTS)") return Result.retry() } -- GitLab From 56deab70ee84d542a45d0770e0342af04b20c38d Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 6 Sep 2023 16:41:44 +0200 Subject: [PATCH 12/14] Version bump to 4.3.6 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c5c034160..078c5c948 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403060001 - versionName '4.3.6-rc.1' + versionCode 403060002 + versionName '4.3.6' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" -- GitLab From f1a1d0efd8cc3b5a34459f3210fdb97dc1079947 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 6 Sep 2023 23:59:58 +0200 Subject: [PATCH 13/14] Fix bug where sync isn't possible as soon there's any connection without INTERNET (bitfireAT/davx5#370) Fix bug where sync wasn't possible as soon there's any connection without INTERNET (bitfireAT/davx5#369) --- .../davdroid/network/ConnectionUtils.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt b/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt index 7fe3eaa65..f54ff2d47 100644 --- a/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/network/ConnectionUtils.kt @@ -47,27 +47,32 @@ object ConnectionUtils { internal fun internetAvailable(context: Context, ignoreVpns: Boolean): Boolean { val connectivityManager = context.getSystemService()!! return connectivityManager.allNetworks.any { network -> - Logger.log.log(Level.FINE, "Looking for validated Internet", connectivityManager.getNetworkInfo(network)) + val capabilities = connectivityManager.getNetworkCapabilities(network) + Logger.log.log(Level.FINE, "Looking for validated Internet over this connection.", + arrayOf(connectivityManager.getNetworkInfo(network), capabilities)) - connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> + if (capabilities != null) { if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { Logger.log.fine("Missing network capability: INTERNET") - return false + return@any false } if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { Logger.log.fine("Missing network capability: VALIDATED") - return false + return@any false } if (ignoreVpns) if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { Logger.log.fine("Missing network capability: NOT_VPN") - return false + return@any false } - /* return */ true - } ?: false + Logger.log.fine("This connection can be used.") + /* return@any */ true + } else + // no network capabilities available, we can't use this connection + /* return@any */ false } } -- GitLab From 0c991288d5bfe6f2dce63ac9d21ab9d7fcd5ca34 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 7 Sep 2023 00:01:05 +0200 Subject: [PATCH 14/14] Bump version to 4.3.6.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 078c5c948..2b22a0d46 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403060002 - versionName '4.3.6' + versionCode 403060100 + versionName '4.3.6.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" -- GitLab