From 262592a3d97ba1ab1c7d9bead29fc53de90a4e25 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Sun, 11 Dec 2022 17:03:27 +0100 Subject: [PATCH 001/160] Remove duplicate copyright header (#218) --- .../at/bitfire/davdroid/servicedetection/DavResourceFinder.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 076c00ccd..814708047 100644 --- a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -1,7 +1,3 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ - /*************************************************************************************************** * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -- GitLab From cfee0f346151c6f04d545f611ea6eff25a831981 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 13 Dec 2022 19:57:34 +0100 Subject: [PATCH 002/160] work manager manual sync (bitfireAT/davx5#109) * rabase with dev branch * added test for checking whether manual work-manager sync queues worker * overwrite getForegroundInfo to show a "sync running" notification, to run expedited work on Android <12 * basic error state with sensible feedback from syncframework * remove integer state flags and pass SyncResult as string * Manual sync cancellation * rabase with dev branch * Minor changes - add Jtx Board sync adapter to sync worker - use new notification ID for sync worker * status bar reflects sync of SyncWorker and sync framework correctly * [WIP] custom hilt SyncComponent * fix autoclose cast not available below api24 * SyncScope implementation using WeakReference * Remove unnecessary logging call * AddressBooksSyncAdapter.sync uses SyncWorker instead of ContentResolve to call requestSync * add some code documentation * move all utility objects into one package * Also check SyncWorker state for accounts list sync status bar * clean up imports * Remove duplicate copyright notices * Minor changes Co-authored-by: Ricki Hirner --- .../davdroid/syncadapter/SyncAdapterTest.kt | 8 +- .../davdroid/syncadapter/SyncWorkerTest.kt | 107 ++++++++++ .../davdroid/syncadapter/TestSyncManager.kt | 2 +- app/src/main/AndroidManifest.xml | 8 + app/src/main/java/at/bitfire/davdroid/App.kt | 1 + .../java/at/bitfire/davdroid/db/Collection.kt | 2 +- .../at/bitfire/davdroid/db/WebDavDocument.kt | 2 +- .../at/bitfire/davdroid/di/SyncComponent.kt | 72 +++++++ .../davdroid/resource/LocalAddressBook.kt | 2 +- .../davdroid/resource/LocalCalendar.kt | 2 +- .../davdroid/resource/LocalJtxCollection.kt | 2 +- .../davdroid/resource/LocalTaskList.kt | 2 +- .../servicedetection/DavResourceFinder.kt | 2 +- .../davdroid/settings/AccountSettings.kt | 2 +- .../AddressBooksSyncAdapterService.kt | 16 +- .../syncadapter/CalendarSyncManager.kt | 2 +- .../CalendarsSyncAdapterService.kt | 8 +- .../syncadapter/ContactsSyncAdapterService.kt | 8 +- .../syncadapter/ContactsSyncManager.kt | 4 +- .../syncadapter/JtxSyncAdapterService.kt | 10 +- .../davdroid/syncadapter/JtxSyncManager.kt | 2 +- .../syncadapter/SyncAdapterService.kt | 32 ++- .../bitfire/davdroid/syncadapter/SyncUtils.kt | 2 +- .../davdroid/syncadapter/SyncWorker.kt | 197 ++++++++++++++++++ .../syncadapter/TasksSyncAdapterService.kt | 10 +- .../davdroid/syncadapter/TasksSyncManager.kt | 2 +- .../davdroid/ui/AccountListFragment.kt | 4 +- .../bitfire/davdroid/ui/AccountsActivity.kt | 4 +- .../at/bitfire/davdroid/ui/HomeSetAdapter.kt | 2 +- .../davdroid/ui/PermissionsFragment.kt | 8 +- .../davdroid/ui/account/AccountActivity.kt | 4 +- .../ui/account/AddressBooksFragment.kt | 2 +- .../davdroid/ui/account/CalendarsFragment.kt | 2 +- .../ui/account/CollectionsFragment.kt | 66 ++++-- .../ui/account/CreateCollectionFragment.kt | 2 +- .../ui/account/RenameAccountFragment.kt | 9 +- .../davdroid/ui/account/SettingsActivity.kt | 2 +- .../davdroid/ui/account/WebcalFragment.kt | 4 +- .../ui/account/WifiPermissionsActivity.kt | 2 +- .../ui/intro/PermissionsIntroFragment.kt | 6 +- .../davdroid/{ => util}/CompatUtils.kt | 2 +- .../davdroid/{ => util}/ConcurrentUtils.kt | 2 +- .../bitfire/davdroid/{ => util}/DavUtils.kt | 30 ++- .../at/bitfire/davdroid/util/LiveDataUtils.kt | 36 ++++ .../davdroid/{ => util}/PermissionUtils.kt | 4 +- .../davdroid/webdav/RandomAccessCallback.kt | 1 + .../webdav/StreamingFileDescriptor.kt | 2 +- .../bitfire/davdroid/ConcurrentUtilsTest.kt | 1 + .../java/at/bitfire/davdroid/DavUtilsTest.kt | 1 + 49 files changed, 571 insertions(+), 132 deletions(-) create mode 100644 app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/di/SyncComponent.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt rename app/src/main/java/at/bitfire/davdroid/{ => util}/CompatUtils.kt (93%) rename app/src/main/java/at/bitfire/davdroid/{ => util}/ConcurrentUtils.kt (97%) rename app/src/main/java/at/bitfire/davdroid/{ => util}/DavUtils.kt (92%) create mode 100644 app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt rename app/src/main/java/at/bitfire/davdroid/{ => util}/PermissionUtils.kt (98%) diff --git a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncAdapterTest.kt b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncAdapterTest.kt index a4a7f03b1..3adcd75d2 100644 --- a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncAdapterTest.kt +++ b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncAdapterTest.kt @@ -49,7 +49,7 @@ class SyncAdapterTest { fun setUp() { hiltRule.inject() - syncAdapter = TestSyncAdapter(context, db) + syncAdapter = TestSyncAdapter(targetContext) } @@ -117,11 +117,11 @@ class SyncAdapterTest { assertEquals(1, syncAdapter.syncCalled.get()) // check whether contextClassLoader is set - assertEquals(context.classLoader, Thread.currentThread().contextClassLoader) + assertEquals(targetContext.classLoader, Thread.currentThread().contextClassLoader) } - class TestSyncAdapter(context: Context, db: AppDatabase): SyncAdapterService.SyncAdapter(context, db) { + class TestSyncAdapter(context: Context): SyncAdapterService.SyncAdapter(context) { companion object { /** @@ -148,4 +148,4 @@ class SyncAdapterTest { } -} \ No newline at end of file +} diff --git a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt new file mode 100644 index 000000000..7b75f6d3e --- /dev/null +++ b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/SyncWorkerTest.kt @@ -0,0 +1,107 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentResolver +import android.content.Context +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Configuration +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.impl.utils.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.NotificationUtils +import com.google.common.util.concurrent.ListenableFuture +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@HiltAndroidTest +class SyncWorkerTest { + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + + private val accountManager = AccountManager.get(context) + private val account = Account("Test Account", context.getString(R.string.account_type)) + private val fakeCredentials = Credentials("test", "test") + + @Before + fun setUp() { + hiltRule.inject() + + assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials))) + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1) + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0) + + // The test application is an instance of HiltTestApplication, which doesn't initialize notification channels. + // However, we need notification channels for the ongoing work notifications. + NotificationUtils.createChannels(context) + + // Initialize WorkManager for instrumentation tests. + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + } + + @After + fun removeAccount() { + accountManager.removeAccountExplicitly(account) + } + + @Test + fun testRequestSync_enqueuesWorker() { + SyncWorker.requestSync(context, account, CalendarContract.AUTHORITY) + val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY) + assertTrue(workScheduledOrRunning(workerName)) + } + + @Test + fun testStopSync_stopsWorker() { + SyncWorker.requestSync(context, account, CalendarContract.AUTHORITY) + SyncWorker.stopSync(context, account, CalendarContract.AUTHORITY) + val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY) + assertFalse(workScheduledOrRunning(workerName)) + + // here we could test whether stopping the work really interrupts the sync thread + } + + private fun workScheduledOrRunning(workerName: String): Boolean { + val future: ListenableFuture> = WorkManager.getInstance(context).getWorkInfosForUniqueWork(workerName) + val workInfoList: List + try { + workInfoList = future.get() + } catch (e: Exception) { + Logger.log.severe("Failed to retrieve work info list for worker $workerName", ) + return false + } + for (workInfo in workInfoList) { + val state = workInfo.state + if (state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED) + return true + } + return false + } + +} \ No newline at end of file diff --git a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/TestSyncManager.kt b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/TestSyncManager.kt index 0c146b63b..a0a9c9762 100644 --- a/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/TestSyncManager.kt +++ b/app/src/androidTestOse/java/at/bitfire/davdroid/syncadapter/TestSyncManager.kt @@ -12,7 +12,7 @@ import at.bitfire.dav4jvm.DavCollection import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.property.GetCTag -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.resource.LocalResource diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 85adf9902..a7113f85a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,14 @@ + + + + + +) { + + private var componentRef: WeakReference? = null + + /** + * Returns a [SyncComponent]. When there is already a known [SyncComponent], + * it will be used. Otherwise, a new one will be created and returned. + * + * It is then stored using a [WeakReference] – so as long as the component + * stays in memory, it will always be returned. When it's not used anymore + * by anyone, it can be removed by garbage collection. After this, it will be + * created again when [get] is called. + * + * @return singleton [SyncComponent] (will be garbage collected when not referenced anymore) + */ + @Synchronized + fun get(): SyncComponent { + val component = componentRef?.get() + + // check for cached component + if (component != null) + return component + + // cached component not available, build new one + val newComponent = provider.get().build() + componentRef = WeakReference(newComponent) + return newComponent + } + +} \ No newline at end of file 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 3244d9553..088df9edc 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -14,7 +14,7 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import android.util.Base64 -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.SyncState 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 bb6e29b53..667760b8e 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -12,7 +12,7 @@ import android.net.Uri import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.log.Logger diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalJtxCollection.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalJtxCollection.kt index 53760c46f..f65a48b85 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalJtxCollection.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalJtxCollection.kt @@ -7,7 +7,7 @@ package at.bitfire.davdroid.resource import android.accounts.Account import android.content.ContentProviderClient import android.content.ContentValues -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.SyncState import at.bitfire.ical4android.JtxCollection diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt index d4198f151..2e2958080 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.kt @@ -10,7 +10,7 @@ import android.content.ContentValues import android.content.Context import android.net.Uri import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.log.Logger diff --git a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 814708047..4f67f978f 100644 --- a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -11,7 +11,7 @@ import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.exception.UnauthorizedException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.log.StringHandler 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 48da84423..645d327ee 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -23,7 +23,7 @@ import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R -import at.bitfire.davdroid.closeCompat +import at.bitfire.davdroid.util.closeCompat import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Credentials diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt index fed850c1f..d1ba112d0 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt @@ -14,8 +14,7 @@ import android.os.Bundle import android.provider.ContactsContract import androidx.core.content.ContextCompat import at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.closeCompat -import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.util.closeCompat import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger @@ -33,13 +32,9 @@ import java.util.logging.Level class AddressBooksSyncAdapterService : SyncAdapterService() { - override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) + override fun syncAdapter() = AddressBooksSyncAdapter(this) - - class AddressBooksSyncAdapter( - context: Context, - appDatabase: AppDatabase - ) : SyncAdapter(context, appDatabase) { + class AddressBooksSyncAdapter(context: Context) : SyncAdapter(context) { @EntryPoint @InstallIn(SingletonComponent::class) @@ -64,10 +59,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { if (updateLocalAddressBooks(account, syncResult)) for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) { Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount) - val syncExtras = Bundle(extras) - syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true) - syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true) - ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras) + SyncWorker.requestSync(context, addressBookAccount) } } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync address books", e) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt index 92d5e4b9b..29625a72e 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt @@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.SyncState diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt index 83633b5ba..902f09ee5 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt @@ -27,13 +27,9 @@ import kotlin.collections.set class CalendarsSyncAdapterService: SyncAdapterService() { - override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase) + override fun syncAdapter() = CalendarsSyncAdapter(this) - - class CalendarsSyncAdapter( - context: Context, - appDatabase: AppDatabase - ) : SyncAdapter(context, appDatabase) { + class CalendarsSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt index 454d06535..87d2fb93b 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt @@ -24,13 +24,9 @@ class ContactsSyncAdapterService: SyncAdapterService() { const val PREVIOUS_GROUP_METHOD = "previous_group_method" } - override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase) + override fun syncAdapter() = ContactsSyncAdapter(this) - - class ContactsSyncAdapter( - context: Context, - appDatabase: AppDatabase - ) : SyncAdapter(context, appDatabase) { + class ContactsSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt index 4df44fc90..c2b3e5397 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt @@ -16,8 +16,8 @@ import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.DavUtils -import at.bitfire.davdroid.DavUtils.sameTypeAs +import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.DavUtils.sameTypeAs import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.SyncState diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt index a5afb7d8b..df1e3aceb 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt @@ -30,13 +30,9 @@ import kotlin.collections.set class JtxSyncAdapterService: SyncAdapterService() { - override fun syncAdapter() = JtxSyncAdapter(this, appDatabase) + override fun syncAdapter() = JtxSyncAdapter(this) - - class JtxSyncAdapter( - context: Context, - appDatabase: AppDatabase - ) : SyncAdapter(context, appDatabase) { + class JtxSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { @@ -113,4 +109,4 @@ class JtxSyncAdapterService: SyncAdapterService() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt index f02b1f5fe..2ff6fb305 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt @@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.SyncState diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.kt index 969cbd25a..71173132b 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncAdapterService.kt @@ -12,19 +12,21 @@ import android.net.NetworkCapabilities import android.net.wifi.WifiManager import android.os.Bundle import androidx.core.content.getSystemService -import at.bitfire.davdroid.ConcurrentUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.InvalidAccountException -import at.bitfire.davdroid.PermissionUtils import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.account.WifiPermissionsActivity -import dagger.hilt.android.AndroidEntryPoint +import at.bitfire.davdroid.util.ConcurrentUtils +import at.bitfire.davdroid.util.PermissionUtils +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import java.util.* import java.util.logging.Level -import javax.inject.Inject -@AndroidEntryPoint abstract class SyncAdapterService: Service() { companion object { @@ -61,9 +63,6 @@ abstract class SyncAdapterService: Service() { const val SYNC_EXTRAS_FULL_RESYNC = "full_resync" } - @Inject lateinit var appDatabase: AppDatabase - - protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!! @@ -78,8 +77,7 @@ abstract class SyncAdapterService: Service() { * Also provides some useful methods that can be used by derived sync adapters. */ abstract class SyncAdapter( - context: Context, - val db: AppDatabase + context: Context ): AbstractThreadedSyncAdapter( context, true // isSyncable shouldn't be -1 because DAVx5 sets it to 0 or 1. @@ -104,6 +102,17 @@ abstract class SyncAdapterService: Service() { } + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncAdapterEntryPoint { + fun appDatabase(): AppDatabase + } + + private val syncAdapterEntryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java) + internal val db = syncAdapterEntryPoint.appDatabase() + + + abstract fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) { @@ -136,8 +145,9 @@ abstract class SyncAdapterService: Service() { } }) Logger.log.log(Level.INFO, "Sync for $currentSyncKey finished", syncResult) - else + else { Logger.log.warning("There's already another running sync for $currentSyncKey, aborting") + } } override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt index a808dd905..4841a7fda 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt @@ -18,7 +18,7 @@ import androidx.annotation.WorkerThread import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.InvalidAccountException -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Service diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt new file mode 100644 index 000000000..9e5059d4e --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -0,0 +1,197 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.provider.CalendarContract +import android.provider.ContactsContract +import androidx.concurrent.futures.CallbackToFutureAdapter +import androidx.core.app.NotificationCompat +import androidx.hilt.work.HiltWorker +import androidx.lifecycle.Transformations +import androidx.work.* +import at.bitfire.davdroid.R +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.ui.NotificationUtils +import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.LiveDataUtils +import at.bitfire.davdroid.util.closeCompat +import at.bitfire.ical4android.TaskProvider +import com.google.common.util.concurrent.ListenableFuture +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.util.logging.Level + +/** + * Handles sync requests + */ +@HiltWorker +class SyncWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters +) : Worker(appContext, workerParams) { + + companion object { + + const val ARG_ACCOUNT_NAME = "accountName" + const val ARG_ACCOUNT_TYPE = "accountType" + const val ARG_AUTHORITY = "authority" + + fun workerName(account: Account, authority: String): String { + return "explicit-sync $authority ${account.type}/${account.name}" + } + + /** + * Requests immediate synchronization of an account with all applicable + * authorities (contacts, calendars, …). + * + * @param account account to sync + */ + fun requestSync(context: Context, account: Account) { + for (authority in DavUtils.syncAuthorities(context)) + requestSync(context, account, authority) + } + + /** + * Requests immediate synchronization of an account with a specific authority. + * + * @param account account to sync + * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]]) + */ + fun requestSync(context: Context, account: Account, authority: String) { + val arguments = Data.Builder() + .putString(ARG_AUTHORITY, authority) + .putString(ARG_ACCOUNT_NAME, account.name) + .putString(ARG_ACCOUNT_TYPE, account.type) + .build() + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(arguments) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + workerName(account, authority), + ExistingWorkPolicy.KEEP, // if sync is already running, just continue + workRequest + ) + } + + fun stopSync(context: Context, account: Account, authority: String) { + WorkManager.getInstance(context).cancelUniqueWork(workerName(account, authority)) + } + + /** + * Will tell whether a worker exists, which belongs to given account and authorities, + * and that is in the given worker state. + * + * @param workState state of worker to match + * @param account the account which the workers belong to + * @param authorities type of sync work + * @return boolean *true* if at least one worker with matching state was found; *false* otherwise + */ + fun isSomeWorkerInState(context: Context, workState: WorkInfo.State, account: Account, authorities: List) = + LiveDataUtils.liveDataLogicOr( + authorities.map { authority -> isWorkerInState(context, workState, account, authority) } + ) + + fun isWorkerInState(context: Context, workState: WorkInfo.State, account: Account, authority: String) = + Transformations.map(WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(workerName(account, authority))) { workInfoList -> + workInfoList.any { workInfo -> workInfo.state == workState } + } + + + } + + + /** thread which runs the actual sync code (can be interrupted to stop synchronization) */ + var syncThread: Thread? = null + + override fun doWork(): Result { + 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") + Logger.log.info("Running sync worker: account=$account, authority=$authority") + + val syncAdapter = when (authority) { + applicationContext.getString(R.string.address_books_authority) -> + AddressBooksSyncAdapterService.AddressBooksSyncAdapter(applicationContext) + CalendarContract.AUTHORITY -> + CalendarsSyncAdapterService.CalendarsSyncAdapter(applicationContext) + ContactsContract.AUTHORITY -> + ContactsSyncAdapterService.ContactsSyncAdapter(applicationContext) + TaskProvider.ProviderName.JtxBoard.authority -> + JtxSyncAdapterService.JtxSyncAdapter(applicationContext) + TaskProvider.ProviderName.OpenTasks.authority, + TaskProvider.ProviderName.TasksOrg.authority -> + TasksSyncAdapterService.TasksSyncAdapter(applicationContext) + else -> + throw IllegalArgumentException("Invalid authority $authority") + } + + // Pass flags to the sync adapter. Note that these are sync framework flags, but they don't + // have anything to do with the sync framework anymore. They only exist because we still use + // the same sync code called from two locations (from the WorkManager and from the sync framework). + val extras = Bundle(2) + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync + extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue) + val result = SyncResult() + + val provider: ContentProviderClient? = + try { + applicationContext.contentResolver.acquireContentProviderClient(authority) + } catch (e: SecurityException) { + Logger.log.log(Level.WARNING, "Missing permissions to acquire ContentProviderClient for $authority", e) + null + } + if (provider == null) { + Logger.log.warning("Couldn't acquire ContentProviderClient for $authority") + return Result.failure() + } + + try { + syncThread = Thread.currentThread() + syncAdapter.onPerformSync(account, extras, authority, provider, result) + } catch (e: SecurityException) { + syncAdapter.onSecurityException(account, extras, authority, result) + } finally { + provider.closeCompat() + } + + if (result.hasError()) + return Result.failure(Data.Builder() + .putString("syncresult", result.toString()) + .putString("syncResultStats", result.stats.toString()) + .build()) + + return Result.success() + } + + override fun onStopped() { + Logger.log.info("Stopping sync thread") + syncThread?.interrupt() + } + + override fun getForegroundInfoAsync(): ListenableFuture = + CallbackToFutureAdapter.getFuture { completer -> + val notification = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_STATUS) + .setSmallIcon(R.drawable.ic_foreground_notify) + .setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title)) + .setContentText(applicationContext.getString(R.string.foreground_service_notify_text)) + .setStyle(NotificationCompat.BigTextStyle()) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + completer.set(ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt index 8b03e8035..f8ebab154 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt @@ -30,13 +30,9 @@ import java.util.logging.Level */ open class TasksSyncAdapterService: SyncAdapterService() { - override fun syncAdapter() = TasksSyncAdapter(this, appDatabase) + override fun syncAdapter() = TasksSyncAdapter(this) - - class TasksSyncAdapter( - context: Context, - appDatabase: AppDatabase, - ) : SyncAdapter(context, appDatabase) { + class TasksSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { @@ -121,4 +117,4 @@ open class TasksSyncAdapterService: SyncAdapterService() { } -} \ No newline at end of file +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt index 1d0fcdc4d..d19f309c6 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt @@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.SyncState diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt index 6efb03a7b..d52bc5f77 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt @@ -26,8 +26,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import at.bitfire.davdroid.DavUtils -import at.bitfire.davdroid.DavUtils.SyncStatus +import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.DavUtils.SyncStatus import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.AccountListBinding import at.bitfire.davdroid.databinding.AccountListItemBinding 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 282ddea3c..db4efcba4 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -15,9 +15,9 @@ import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.getSystemService import androidx.core.view.GravityCompat -import at.bitfire.davdroid.DavUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.ActivityAccountsBinding +import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.ui.intro.IntroActivity import at.bitfire.davdroid.ui.setup.LoginActivity import com.google.android.material.navigation.NavigationView @@ -111,7 +111,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele val accounts = allAccounts() for (account in accounts) - DavUtils.requestSync(this, account) + SyncWorker.requestSync(this, account) } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/HomeSetAdapter.kt b/app/src/main/java/at/bitfire/davdroid/ui/HomeSetAdapter.kt index b8f50ca73..a09f5d49d 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/HomeSetAdapter.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/HomeSetAdapter.kt @@ -13,7 +13,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Filter import android.widget.TextView -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.db.HomeSet diff --git a/app/src/main/java/at/bitfire/davdroid/ui/PermissionsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/PermissionsFragment.kt index 5d3b3cc40..4ea253d27 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/PermissionsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/PermissionsFragment.kt @@ -25,10 +25,10 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.PackageChangedReceiver -import at.bitfire.davdroid.PermissionUtils -import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS -import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMISSIONS -import at.bitfire.davdroid.PermissionUtils.havePermissions +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS +import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS +import at.bitfire.davdroid.util.PermissionUtils.havePermissions import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.ActivityPermissionsBinding import at.bitfire.ical4android.TaskProvider 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 607f25afd..56dfc97d2 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 @@ -20,8 +20,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentStatePagerAdapter import androidx.lifecycle.* -import at.bitfire.davdroid.DavUtils import at.bitfire.davdroid.R +import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.databinding.ActivityAccountBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection @@ -88,7 +88,7 @@ class AccountActivity: AppCompatActivity() { }) binding.sync.setOnClickListener { - DavUtils.requestSync(this, model.account) + SyncWorker.requestSync(this, model.account) Snackbar.make(binding.viewPager, R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show() } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt index ff5e8741d..2a30b1304 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt @@ -7,7 +7,7 @@ package at.bitfire.davdroid.ui.account import android.content.Intent import android.view.* import androidx.fragment.app.FragmentManager -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.AccountCarddavItemBinding import at.bitfire.davdroid.db.Collection diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/CalendarsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/CalendarsFragment.kt index a74d53a48..cbd7916dc 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/CalendarsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/CalendarsFragment.kt @@ -8,7 +8,7 @@ import android.content.Intent import android.view.* import androidx.fragment.app.FragmentManager import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.AccountCaldavItemBinding import at.bitfire.davdroid.db.Collection diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/CollectionsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/CollectionsFragment.kt index 570c81daf..fbea9b205 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/CollectionsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/CollectionsFragment.kt @@ -31,7 +31,9 @@ import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.ui.PermissionsActivity +import at.bitfire.davdroid.util.LiveDataUtils import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -305,36 +307,55 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList // observe RefreshCollectionsWorker status val isRefreshing = RefreshCollectionsWorker.isWorkerInState(context, serviceId, WorkInfo.State.RUNNING) - // observe whether sync is active - private var syncStatusHandle: Any? = null - val isSyncActive = MutableLiveData() - val isSyncPending = MutableLiveData() - + // observe whether sync framework is active + private var syncFrameworkStatusHandle: Any? = null + private val isSyncFrameworkActive = MutableLiveData() + private val isSyncFrameworkPending = MutableLiveData() + + // observe SyncWorker state + private val authorities = + if (collectionType == Collection.TYPE_ADDRESSBOOK) + listOf(context.getString(R.string.address_books_authority), ContactsContract.AUTHORITY) + else + listOf(CalendarContract.AUTHORITY, taskProvider?.authority).filterNotNull() + private val isSyncWorkerRunning = SyncWorker.isSomeWorkerInState(context, + WorkInfo.State.RUNNING, + accountModel.account, + authorities) + private val isSyncWorkerEnqueued = SyncWorker.isSomeWorkerInState(context, + WorkInfo.State.ENQUEUED, + accountModel.account, + authorities) + + // observe and combine states of sync framework and SyncWorker + val isSyncActive = LiveDataUtils.liveDataLogicOr(listOf(isSyncFrameworkActive, isSyncWorkerRunning)) + val isSyncPending = LiveDataUtils.liveDataLogicOr(listOf(isSyncFrameworkPending, isSyncWorkerEnqueued)) init { viewModelScope.launch(Dispatchers.Default) { - syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model) - checkSyncStatus() + syncFrameworkStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model) + checkSyncFrameworkStatus() } } override fun onCleared() { - syncStatusHandle?.let { ContentResolver.removeStatusChangeListener(it) } - } - - fun refresh() { - RefreshCollectionsWorker.refreshCollections(context, serviceId) + syncFrameworkStatusHandle?.let { + ContentResolver.removeStatusChangeListener(it) + } } @AnyThread override fun onStatusChanged(which: Int) { - checkSyncStatus() + checkSyncFrameworkStatus() } @AnyThread @Synchronized - private fun checkSyncStatus() { + private fun checkSyncFrameworkStatus() { + // SyncFramework only, isSyncFrameworkActive/Pending gets combined in logic OR with SyncWorker state if (collectionType == Collection.TYPE_ADDRESSBOOK) { + // CardDAV tab val mainAuthority = context.getString(R.string.address_books_authority) val mainSyncActive = ContentResolver.isSyncActive(accountModel.account, mainAuthority) val mainSyncPending = ContentResolver.isSyncPending(accountModel.account, mainAuthority) @@ -343,22 +364,31 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList val syncActive = addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) } val syncPending = addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) } - isSyncActive.postValue(mainSyncActive || syncActive) - isSyncPending.postValue(mainSyncPending || syncPending) + isSyncFrameworkActive.postValue(mainSyncActive || syncActive) + isSyncFrameworkPending.postValue(mainSyncPending || syncPending) + } else { + // CalDAV tab val authorities = mutableListOf(CalendarContract.AUTHORITY) taskProvider?.let { authorities += it.authority } - isSyncActive.postValue(authorities.any { + isSyncFrameworkActive.postValue(authorities.any { ContentResolver.isSyncActive(accountModel.account, it) }) - isSyncPending.postValue(authorities.any { + isSyncFrameworkPending.postValue(authorities.any { ContentResolver.isSyncPending(accountModel.account, it) }) } } + + // actions + + fun refresh() { + RefreshCollectionsWorker.refreshCollections(context, serviceId) + } + } } \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt index 39ac27cd6..1dfa59676 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt @@ -15,7 +15,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.* import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.XmlUtils -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt index b45c2c5ec..bb858c50b 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt @@ -27,16 +27,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import at.bitfire.davdroid.DavUtils -import at.bitfire.davdroid.InvalidAccountException -import at.bitfire.davdroid.R -import at.bitfire.davdroid.closeCompat +import at.bitfire.davdroid.* import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTaskList import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener +import at.bitfire.davdroid.util.closeCompat import at.bitfire.ical4android.TaskProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -220,7 +219,7 @@ class RenameAccountFragment: DialogFragment() { } // synchronize again - DavUtils.requestSync(context, newAccount) + SyncWorker.requestSync(context, newAccount) } } 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 b4a58e02f..eaa08c03d 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 @@ -27,7 +27,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.preference.* import at.bitfire.davdroid.InvalidAccountException -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/WebcalFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/WebcalFragment.kt index b89bc0ca3..107674389 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/WebcalFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/WebcalFragment.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.* import androidx.room.Transaction import at.bitfire.dav4jvm.UrlUtils import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R -import at.bitfire.davdroid.closeCompat +import at.bitfire.davdroid.util.closeCompat import at.bitfire.davdroid.databinding.AccountCaldavItemBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt index 679ce8fb3..b9d8bd3ba 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt @@ -26,7 +26,7 @@ import androidx.core.location.LocationManagerCompat import androidx.core.text.HtmlCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.ActivityWifiPermissionsBinding import at.bitfire.davdroid.log.Logger diff --git a/app/src/main/java/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt index 399d19072..0588aefce 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt @@ -10,9 +10,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import at.bitfire.davdroid.PermissionUtils -import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS -import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMISSIONS +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS +import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS import at.bitfire.davdroid.R import at.bitfire.ical4android.TaskProvider import javax.inject.Inject diff --git a/app/src/main/java/at/bitfire/davdroid/CompatUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt similarity index 93% rename from app/src/main/java/at/bitfire/davdroid/CompatUtils.kt rename to app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt index d705c8974..d29c1a18c 100644 --- a/app/src/main/java/at/bitfire/davdroid/CompatUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/util/CompatUtils.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.util import android.content.ContentProviderClient import android.os.Build diff --git a/app/src/main/java/at/bitfire/davdroid/ConcurrentUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/ConcurrentUtils.kt similarity index 97% rename from app/src/main/java/at/bitfire/davdroid/ConcurrentUtils.kt rename to app/src/main/java/at/bitfire/davdroid/util/ConcurrentUtils.kt index 57d41ff71..2163e3783 100644 --- a/app/src/main/java/at/bitfire/davdroid/ConcurrentUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/util/ConcurrentUtils.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.util import java.util.* diff --git a/app/src/main/java/at/bitfire/davdroid/DavUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/DavUtils.kt similarity index 92% rename from app/src/main/java/at/bitfire/davdroid/DavUtils.kt rename to app/src/main/java/at/bitfire/davdroid/util/DavUtils.kt index db9a195bc..864b30cd3 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/util/DavUtils.kt @@ -2,20 +2,23 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.util import android.accounts.Account import android.content.ContentResolver import android.content.Context import android.net.ConnectivityManager import android.os.Build -import android.os.Bundle import android.provider.CalendarContract import android.provider.ContactsContract import androidx.core.content.getSystemService +import androidx.work.WorkInfo +import at.bitfire.davdroid.Android10Resolver +import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.syncadapter.SyncWorker import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -178,27 +181,22 @@ object DavUtils { if (addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) }) return SyncStatus.ACTIVE - // check get pending syncs + // check pending syncs if (authorities.any { ContentResolver.isSyncPending(account, it) } || addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) }) return SyncStatus.PENDING + // Also check SyncWorkers + val pending = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.ENQUEUED, account, authorities.toList()).value + if (pending != null && pending == true) + return SyncStatus.PENDING + val running = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.RUNNING, account, authorities.toList()).value + if (running != null && running == true) + return SyncStatus.ACTIVE + return SyncStatus.IDLE } - /** - * Requests an immediate, manual sync of all available authorities for the given account. - * - * @param account account to sync - */ - fun requestSync(context: Context, account: Account) { - for (authority in syncAuthorities(context)) { - val extras = Bundle(2) - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync - extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue) - ContentResolver.requestSync(account, authority, extras) - } - } /** * Returns a list of all available sync authorities for main accounts (!= address book accounts): diff --git a/app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt new file mode 100644 index 000000000..183b52a75 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt @@ -0,0 +1,36 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +object LiveDataUtils { + + /** + * Combines multiple [LiveData] inputs with logical OR to another [LiveData]. + * + * It's value is *null* as soon as no input is added or as long as no input + * has emitted a value. As soon as at least one input has emitted a value, + * the value of the combined object becomes *true* or *false*. + * + * @param inputs inputs to be combined with logical OR + * @return [LiveData] that is *true* when at least one input becomes *true*; *false* otherwise + */ + fun liveDataLogicOr(inputs: Iterable>) = object : MediatorLiveData() { + init { + inputs.forEach { liveData -> + addSource(liveData) { + recalculate() + } + } + } + + fun recalculate() { + value = inputs.any { it.value == true } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/PermissionUtils.kt b/app/src/main/java/at/bitfire/davdroid/util/PermissionUtils.kt similarity index 98% rename from app/src/main/java/at/bitfire/davdroid/PermissionUtils.kt rename to app/src/main/java/at/bitfire/davdroid/util/PermissionUtils.kt index cc1df6689..3d081833d 100644 --- a/app/src/main/java/at/bitfire/davdroid/PermissionUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/util/PermissionUtils.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.util import android.Manifest import android.app.PendingIntent @@ -16,6 +16,8 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.PermissionsActivity diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt b/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt index e0406f795..1a8db3348 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt @@ -23,6 +23,7 @@ import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.davdroid.* import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.ui.NotificationUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.webdav.cache.MemoryCache import at.bitfire.davdroid.webdav.cache.SegmentedCache import okhttp3.Headers diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt index 4c91972ee..ec016cd3c 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -12,7 +12,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.exception.HttpException -import at.bitfire.davdroid.DavUtils +import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger diff --git a/app/src/test/java/at/bitfire/davdroid/ConcurrentUtilsTest.kt b/app/src/test/java/at/bitfire/davdroid/ConcurrentUtilsTest.kt index c6c487c4d..c302ab04a 100644 --- a/app/src/test/java/at/bitfire/davdroid/ConcurrentUtilsTest.kt +++ b/app/src/test/java/at/bitfire/davdroid/ConcurrentUtilsTest.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid +import at.bitfire.davdroid.util.ConcurrentUtils import org.junit.Assert.assertEquals import org.junit.Test import java.util.concurrent.atomic.AtomicInteger diff --git a/app/src/test/java/at/bitfire/davdroid/DavUtilsTest.kt b/app/src/test/java/at/bitfire/davdroid/DavUtilsTest.kt index 30b135048..0a64cab91 100644 --- a/app/src/test/java/at/bitfire/davdroid/DavUtilsTest.kt +++ b/app/src/test/java/at/bitfire/davdroid/DavUtilsTest.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid +import at.bitfire.davdroid.util.DavUtils import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Assert.* import org.junit.Test -- GitLab From 2730d14cb9da9da8a8ef935a4f387d38e9da627f Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 15 Dec 2022 15:51:06 +0100 Subject: [PATCH 003/160] Version bump to 4.3-alpha.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bb7a5793c..74a0d9e6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 402060000 - versionName '4.2.6' + versionCode 403000001 + versionName '4.3-alpha.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" setProperty "archivesBaseName", "davx5-ose-" + getVersionName() -- GitLab From 0521f4bcf0904856f6a81198a8ad6a0ece3ea014 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Thu, 15 Dec 2022 23:49:36 +0100 Subject: [PATCH 004/160] Trivial indentation fixes (#226) --- .../bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt | 2 +- .../bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt | 2 +- .../at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt index 902f09ee5..27190dc42 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt @@ -29,7 +29,7 @@ class CalendarsSyncAdapterService: SyncAdapterService() { override fun syncAdapter() = CalendarsSyncAdapter(this) - class CalendarsSyncAdapter(context: Context) : SyncAdapter(context) { + class CalendarsSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt index 87d2fb93b..5f16cbb71 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt @@ -26,7 +26,7 @@ class ContactsSyncAdapterService: SyncAdapterService() { override fun syncAdapter() = ContactsSyncAdapter(this) - class ContactsSyncAdapter(context: Context) : SyncAdapter(context) { + class ContactsSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt index f8ebab154..fe482682f 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt @@ -32,7 +32,7 @@ open class TasksSyncAdapterService: SyncAdapterService() { override fun syncAdapter() = TasksSyncAdapter(this) - class TasksSyncAdapter(context: Context) : SyncAdapter(context) { + class TasksSyncAdapter(context: Context) : SyncAdapter(context) { override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) { try { -- GitLab From 1f818ee3b4f2cc88942eed957a23a52b3306109a Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:22:10 +0100 Subject: [PATCH 005/160] Added language selection (bitfireAT/davx5#137) * added language selection * PermissionsFragment: Hide notification permission switch on Android < 13 * Retrieve locales with a function * Added locales flavoring Signed-off-by: Arnau Mora * Moved locales functions to `locales.gradle` Signed-off-by: Arnau Mora * Added `locales_config.xml` generation Signed-off-by: Arnau Mora * Now gets generated automatically Signed-off-by: Arnau Mora * Added `android:localeConfig` Signed-off-by: Arnau Mora * Updated submodules Signed-off-by: Arnau Mora * Using `logger` instead of `println` Signed-off-by: Arnau Mora Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner Co-authored-by: Arnau Mora --- app/build.gradle | 17 ++- app/locales.gradle | 105 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 14 ++- .../at/bitfire/davdroid/settings/Settings.kt | 3 + .../davdroid/ui/AppSettingsActivity.kt | 34 ++++++ app/src/main/res/drawable/ic_language.xml | 5 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings_app.xml | 6 + 8 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 app/locales.gradle create mode 100644 app/src/main/res/drawable/ic_language.xml diff --git a/app/build.gradle b/app/build.gradle index 74a0d9e6d..c128a094c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,8 @@ apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply from: 'locales.gradle' + android { compileSdkVersion 33 buildToolsVersion '33.0.0' @@ -33,6 +35,16 @@ android { arg("room.schemaLocation", "$projectDir/schemas") } } + + applicationVariants.all { variant -> + def locales = getLocales(variant.flavorName) + + variant.buildConfigField "String[]", "TRANSLATION_ARRAY", "new String[]{\"" + locales.join("\",\"") + "\"}" + variant.mergedFlavor.resourceConfigurations.clear() + variant.mergedFlavor.resourceConfigurations.addAll(locales) + + generateLocalesConfig(variant.flavorName, locales) + } } compileOptions { @@ -62,6 +74,9 @@ android { } sourceSets { + ose.res.srcDirs += "build/generated/res/locale-ose" + + androidTest.java.srcDirs = [ "src/androidTest/java" ] androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } @@ -110,7 +125,7 @@ dependencies { implementation "com.google.dagger:hilt-android:${versions.hilt}" kapt "com.google.dagger:hilt-android-compiler:${versions.hilt}" - implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.appcompat:appcompat:1.6.0-rc01' implementation 'androidx.browser:browser:1.4.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' diff --git a/app/locales.gradle b/app/locales.gradle new file mode 100644 index 000000000..cc271bb3b --- /dev/null +++ b/app/locales.gradle @@ -0,0 +1,105 @@ +import groovy.xml.MarkupBuilder + +import static groovy.io.FileType.DIRECTORIES + +/** + * Obtains a list of all the available locales for an specific flavor + * @since 20221123 + * @param flavorDir The base directory of the flavor inside `/app/src` + * @return A list with the language codes of the locales available. + */ +Set getLocalesForFlavor(File flavorDir) { + def dir = new File(flavorDir, "res") + + if (!flavorDir.exists()) { + logger.warn("Tried to get locales for non-existing flavor. Directory: $flavorDir") + return new LinkedHashSet() + } + if (!dir.exists()) { + logger.warn("Tried to get locales for a flavor without strings. Directory: $dir") + return new LinkedHashSet() + } + + // Initialize the list English, since it's available by default + Set locales = new LinkedHashSet(['en']) + + // Get all directories inside resources + logger.trace("Getting locales values directories from $dir") + dir.traverse(type: DIRECTORIES, maxDepth: 0) { file -> + // Get only values directories + def fileName = file.name + if (!fileName.startsWith("values-")) return + + // Take only the values directories that contain strings + def stringsFile = new File(file, "strings.xml") + if (!stringsFile.exists()) return + + // Add to the list the locale of the strings file + def langCode = fileName.substring(fileName.indexOf('-') + 1) + locales.add(langCode) + } + + // Log the available locales + logger.info('Supported locales: ' + locales.join(", ")) + + // Return the built list + return locales +} + +/** + * Obtains a list of all the available locales + * @since 20220928 + * @return A list with the language codes of the locales available. + */ +Set getLocales(String flavor) { + // Get all the flavor directories + def dir = new File(projectDir, "src") + + // Get a list of locales for the base flavor + def mainDir = new File(dir, 'main') + logger.trace("Getting main locales ($mainDir)...") + def mainLocales = getLocalesForFlavor(mainDir) + + // Get the current flavor + def flavorDir = new File(dir, flavor) + logger.trace("Getting locales for flavor $flavor ($flavorDir)...") + def flavorLocales = getLocalesForFlavor(flavorDir) + + // Build the locales list + // We use Set for avoiding duplicates + Set locales = new LinkedHashSet() + locales.addAll(mainLocales) + locales.addAll(flavorLocales) + + // Log the available locales + logger.trace("Supported locales for flavor $flavor: " + locales.join(', ')) + + return locales +} + +def generateLocalesConfig(String flavor, Set locales) { + def outputDir = new File(projectDir, "build/generated/res/locale-$flavor/xml") + mkdir outputDir + + logger.trace("Generating locales_config.xml...") + new File(outputDir, "locales_config.xml").withWriter { writer -> + def destXml = new MarkupBuilder(new IndentPrinter(writer, " ", true, true)) + destXml.setDoubleQuotes(true) + def destXmlMkp = destXml.getMkp() + destXmlMkp.xmlDeclaration(version: "1.0", encoding: "utf-8") + destXmlMkp.comment("Generated at ${new Date()}") + destXmlMkp.yield "\r\n" + + destXml."locale-config"(['xmlns:android': "http://schemas.android.com/apk/res/android"]) { + locales.forEach { locale -> + destXml."locale"("android:name": locale) + } + } + } +} + +// Export getLocales and generateLocalesConfig +ext { + getLocales = this.&getLocales + generateLocalesConfig = this.&generateLocalesConfig +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a7113f85a..5b800c718 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,7 +48,8 @@ android:theme="@style/AppTheme" android:resizeableActivity="true" tools:ignore="UnusedAttribute" - android:supportsRtl="true"> + android:supportsRtl="true" + android:localeConfig="@xml/locales_config"> + + + + + + 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 31d058fcf..8f2a07224 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/Settings.kt @@ -34,6 +34,9 @@ object Settings { const val PREFERRED_THEME = "preferred_theme" const val PREFERRED_THEME_DEFAULT = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + const val LANGUAGE = "language" + const val LANGUAGE_SYSTEM = "language_system" + const val PREFERRED_TASKS_PROVIDER = "preferred_tasks_provider" /** whether detected collections are selected for synchronization for default */ 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 78cdb1dda..f76962252 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -15,6 +15,7 @@ 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.davdroid.BuildConfig @@ -32,6 +33,7 @@ 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 @@ -227,6 +229,38 @@ class AppSettingsActivity: AppCompatActivity() { false } } + findPreference(Settings.LANGUAGE)!!.apply { + val languageOptions = mutableListOf(null) + for (language in BuildConfig.TRANSLATION_ARRAY) { + languageOptions.add(Locale.forLanguageTag(language)) + } + this.entries = languageOptions.map { language -> language?.displayLanguage ?: context.getText(R.string.app_settings_language_system_default) }.toTypedArray() + this.entryValues = languageOptions.map { language -> language?.language ?: Settings.LANGUAGE_SYSTEM }.toTypedArray() + + val appCompatLocales = AppCompatDelegate.getApplicationLocales() + var currentLocale: Locale? = null + if(!appCompatLocales.isEmpty) { + for (i in 0 until appCompatLocales.size()) { + val locale = appCompatLocales[i] ?: continue + if (languageOptions.contains(appCompatLocales[i]!!)) { + currentLocale = locale + break + } + } + } + setValueIndex(entryValues.indexOf(currentLocale?.language ?: Settings.LANGUAGE_SYSTEM)) + summary = entry + + onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if(newValue.toString() == Settings.LANGUAGE_SYSTEM) + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) + else { + val newLanguage = Locale.forLanguageTag(newValue.toString()) + AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(newLanguage)) + } + false + } + } // integration settings findPreference(Settings.PREFERRED_TASKS_PROVIDER)!!.apply { diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml new file mode 100644 index 000000000..a5061aeb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_language.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9a73f370..1ca08c9e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,8 @@ Notification settings Manage notification channels and their settings Select theme + Select language + System default System default Light diff --git a/app/src/main/res/xml/settings_app.xml b/app/src/main/res/xml/settings_app.xml index 01fd12cc3..28506875d 100644 --- a/app/src/main/res/xml/settings_app.xml +++ b/app/src/main/res/xml/settings_app.xml @@ -95,6 +95,12 @@ android:entryValues="@array/app_settings_theme_values" android:persistent="false" /> + + Date: Tue, 20 Dec 2022 19:03:21 +0100 Subject: [PATCH 006/160] WorkManager address book sync: don't run calendar/tasks sync on address book accounts; vcard4android: set N:LastName for vCard3 groups --- .../davdroid/syncadapter/AddressBooksSyncAdapterService.kt | 2 +- vcard4android | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt index d1ba112d0..f083af9a1 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt @@ -59,7 +59,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { if (updateLocalAddressBooks(account, syncResult)) for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) { Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount) - SyncWorker.requestSync(context, addressBookAccount) + SyncWorker.requestSync(context, addressBookAccount, ContactsContract.AUTHORITY) } } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync address books", e) diff --git a/vcard4android b/vcard4android index c419d7cad..a6e9dc4bc 160000 --- a/vcard4android +++ b/vcard4android @@ -1 +1 @@ -Subproject commit c419d7cad2789bd9e1571d12f31b4807a676ca3d +Subproject commit a6e9dc4bc29690d1a236f879dc42188d34892e74 -- GitLab From 12bb09ecb8bb8f24b8642577f6cbe6dde29089fb Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 21 Dec 2022 17:17:19 +0100 Subject: [PATCH 007/160] Minor CI changes --- .github/workflows/release.yml | 6 +++--- .github/workflows/test-dev.yml | 19 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6138883c0..c2aae01cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,16 +15,16 @@ jobs: submodules: true - uses: actions/setup-java@v2 with: - distribution: 'temurin' + distribution: temurin java-version: 11 - cache: 'gradle' + cache: gradle - uses: gradle/wrapper-validation-action@v1 - name: Prepare keystore run: echo ${{ secrets.android_keystore_base64 }} | base64 -d >$GITHUB_WORKSPACE/keystore.jks - name: Build signed package - run: ./gradlew app:assembleRelease + run: ./gradlew --no-daemon app:assembleRelease env: ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks ANDROID_KEYSTORE_PASSWORD: ${{ secrets.android_keystore_password }} diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 503eeb7cb..b41af2842 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -10,13 +10,13 @@ jobs: submodules: true - uses: actions/setup-java@v2 with: - distribution: zulu + distribution: temurin java-version: 11 cache: gradle - uses: gradle/wrapper-validation-action@v1 - name: Run lint and unit tests - run: ./gradlew app:check + run: ./gradlew --no-daemon app:check - name: Archive results uses: actions/upload-artifact@v2 with: @@ -38,20 +38,17 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - uses: gradle/wrapper-validation-action@v1 - - - name: Cache gradle dependencies - uses: actions/cache@v2 + - uses: actions/setup-java@v2 with: - key: ${{ runner.os }} - path: | - ~/.gradle/caches - ~/.gradle/wrapper + distribution: temurin + java-version: 11 + cache: gradle + - uses: gradle/wrapper-validation-action@v1 - name: Start emulator run: start-emulator.sh - name: Run connected tests - run: ./gradlew app:connectedCheck + run: ./gradlew --no-daemon app:connectedCheck - name: Archive results if: always() uses: actions/upload-artifact@v2 -- GitLab From 1f4398b21ecc8ddd0090edaa69fdbd0a5dda785f Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Mon, 12 Dec 2022 11:37:37 +0100 Subject: [PATCH 008/160] Use consistent margins between different permission switches --- app/src/main/res/layout/activity_permissions.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/activity_permissions.xml b/app/src/main/res/layout/activity_permissions.xml index e363aae19..9c7956b0a 100644 --- a/app/src/main/res/layout/activity_permissions.xml +++ b/app/src/main/res/layout/activity_permissions.xml @@ -120,7 +120,7 @@ style="@style/TextAppearance.MaterialComponents.Body1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="16dp" + android:layout_marginTop="@dimen/card_margin_title_text" android:text="@string/permissions_all_title" android:textAlignment="viewStart" android:textStyle="bold" @@ -223,7 +223,7 @@ style="@style/TextAppearance.MaterialComponents.Body1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="16dp" + android:layout_marginTop="@dimen/card_margin_title_text" android:text="@string/permissions_contacts_title" android:textAlignment="viewStart" app:layout_constraintBottom_toTopOf="@id/contactsStatus" -- GitLab From 53bd4866c3c4a23a24a1e8ebf185d6589814145e Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sun, 1 Jan 2023 23:25:30 +0100 Subject: [PATCH 009/160] Added Mastodon link to navigation drawer (bitfireAT/davx5#179) * Added mastodon icon Signed-off-by: Arnau Mora * Added mastodon link to drawer Signed-off-by: Arnau Mora * Added mastodon link action Signed-off-by: Arnau Mora * Mastodon icon tint Signed-off-by: Arnau Mora * Added Mastodon icon to main Signed-off-by: Arnau Mora Signed-off-by: Arnau Mora --- .../at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt | 5 +++++ app/src/main/res/drawable/mastodon.xml | 8 ++++++++ app/src/main/res/menu/activity_accounts_drawer.xml | 5 +++++ app/src/main/res/values/styles.xml | 2 ++ 4 files changed, 20 insertions(+) create mode 100644 app/src/main/res/drawable/mastodon.xml diff --git a/app/src/main/java/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt b/app/src/main/java/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt index fe513b494..d02d01226 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt @@ -31,6 +31,11 @@ class OseAccountsDrawerHandler @Inject constructor(): BaseAccountsDrawerHandler( activity, Uri.parse("https://twitter.com/" + activity.getString(R.string.twitter_handle)) ) + R.id.nav_mastodon -> + UiUtils.launchUri( + activity, + Uri.parse("https://fosstodon.org/@davx5app") + ) R.id.nav_webdav_mounts -> activity.startActivity(Intent(activity, WebdavMountsActivity::class.java)) diff --git a/app/src/main/res/drawable/mastodon.xml b/app/src/main/res/drawable/mastodon.xml new file mode 100644 index 000000000..4074145b8 --- /dev/null +++ b/app/src/main/res/drawable/mastodon.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_accounts_drawer.xml b/app/src/main/res/menu/activity_accounts_drawer.xml index 7153fc037..0162f7748 100644 --- a/app/src/main/res/menu/activity_accounts_drawer.xml +++ b/app/src/main/res/menu/activity_accounts_drawer.xml @@ -25,6 +25,11 @@ android:icon="@drawable/twitter" android:title="\@davx5app" tools:ignore="HardcodedText"/> + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fe7350c45..127089965 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -35,6 +35,8 @@ @color/grey200 @color/red700 + #6364FF +