Compare commits

..

2 Commits

Author SHA1 Message Date
Ricki Hirner
a89483d8c7 [WIP] synctools: use new builders 2025-08-12 20:55:06 +02:00
Ricki Hirner
60eba44541 Rename 2025-08-09 17:26:49 +02:00
42 changed files with 159 additions and 440 deletions

View File

@@ -8,7 +8,4 @@ updates:
schedule:
interval: "weekly"
commit-message:
prefix: "[CI] "
groups:
ci-actions:
patterns: ["*"]
prefix: "[CI] "

View File

@@ -28,8 +28,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- uses: actions/setup-java@v5
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

View File

@@ -19,8 +19,8 @@ jobs:
discussions: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

View File

@@ -15,8 +15,8 @@ jobs:
if: ${{ github.ref == 'refs/heads/main-ose' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
@@ -35,8 +35,8 @@ jobs:
name: Lint and unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
@@ -56,8 +56,8 @@ jobs:
name: Instrumented tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

View File

@@ -19,8 +19,8 @@ android {
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 405040002
versionName = "4.5.4-rc.1"
versionCode = 405040000
versionName = "4.5.4-alpha.1"
base.archivesName = "davx5-ose-$versionName"

View File

@@ -44,11 +44,6 @@ abstract class DatabaseMigrationTest(
/**
* Used for testing the migration process from [toVersion]-1 to [toVersion].
*
* Note: SQLite's foreign key constraint enforcement is not enabled in tests. We need
* to enable it ourselves using setting "PRAGMA foreign_keys=ON" directly after opening
* a new database connection (works per connection). In tests it's usually more practical
* not to do so, however. In production database connections room enables it for us.
*
* @param prepare Callback to prepare the database. Will be run with database schema in version [toVersion] - 1.
* @param validate Callback to validate the migration result. Will be run with database schema in version [toVersion].
*/
@@ -66,8 +61,6 @@ abstract class DatabaseMigrationTest(
// Prepare the database with the initial version.
val dbName = "test"
helper.createDatabase(dbName, version = toVersion - 1).apply {
// We could enable foreign key constraint enforcement here
// by setting "PRAGMA foreign_keys=ON".
prepare(this)
close()
}

View File

@@ -8,18 +8,14 @@ import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.content.Entity
import android.provider.CalendarContract
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.bitfire.synctools.storage.calendar.AndroidCalendar
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
import at.bitfire.synctools.storage.calendar.AndroidEvent2
import at.bitfire.synctools.test.InitCalendarProviderRule
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@@ -29,8 +25,6 @@ import net.fortuna.ical4j.model.property.RecurrenceId
import net.fortuna.ical4j.model.property.Status
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -50,7 +44,6 @@ class LocalCalendarTest {
lateinit var localCalendarFactory: LocalCalendar.Factory
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
private lateinit var androidCalendar: AndroidCalendar
private lateinit var client: ContentProviderClient
private lateinit var calendar: LocalCalendar
@@ -62,13 +55,12 @@ class LocalCalendarTest {
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
val provider = AndroidCalendarProvider(account, client)
androidCalendar = provider.createAndGetCalendar(ContentValues())
calendar = localCalendarFactory.create(androidCalendar)
calendar = localCalendarFactory.create(provider.createAndGetCalendar(ContentValues()))
}
@After
fun tearDown() {
androidCalendar.delete()
calendar.androidCalendar.delete()
client.closeCompat()
}
@@ -143,94 +135,24 @@ class LocalCalendarTest {
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
val localEvent = calendar.findByName("filename.ics")!!
val eventUrl = androidCalendar.eventUri(localEvent.id)
val eventId = localEvent.id
// set event as dirty
client.update(eventUrl, contentValuesOf(
Events.DIRTY to 1
), null, null)
client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
put(Events.DIRTY, 1)
}, null, null)
// this method should mark the event as deleted
calendar.deleteDirtyEventsWithoutInstances()
// verify that event is not marked as deleted
client.query(eventUrl, arrayOf(Events.DELETED), null, null, null)!!.use { cursor ->
client.query(
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
arrayOf(Events.DELETED), null, null, null
)!!.use { cursor ->
cursor.moveToNext()
assertEquals(0, cursor.getInt(0))
}
}
/**
* Verifies that [LocalCalendar.removeNotDirtyMarked] works as expected.
* @param contentValues values to set on the event. Required:
* - [Events._ID]
* - [Events.DIRTY]
*/
private fun testRemoveNotDirtyMarked(contentValues: ContentValues) {
val id = androidCalendar.addEvent(Entity(
contentValuesOf(
Events.CALENDAR_ID to androidCalendar.id,
Events.DTSTART to System.currentTimeMillis(),
Events.DTEND to System.currentTimeMillis(),
Events.TITLE to "Some Event",
AndroidEvent2.COLUMN_FLAGS to 123
).apply { putAll(contentValues) }
))
calendar.removeNotDirtyMarked(123)
assertNull(androidCalendar.getEvent(id))
}
@Test
fun testRemoveNotDirtyMarked_IdLargerThanIntMaxValue() = testRemoveNotDirtyMarked(
contentValuesOf(Events._ID to Int.MAX_VALUE.toLong() + 10, Events.DIRTY to 0)
)
@Test
fun testRemoveNotDirtyMarked_DirtyIs0() = testRemoveNotDirtyMarked(
contentValuesOf(Events._ID to 1, Events.DIRTY to 0)
)
@Test
fun testRemoveNotDirtyMarked_DirtyNull() = testRemoveNotDirtyMarked(
contentValuesOf(Events._ID to 1, Events.DIRTY to null)
)
/**
* Verifies that [LocalCalendar.markNotDirty] works as expected.
* @param contentValues values to set on the event. Required:
* - [Events.DIRTY]
*/
private fun testMarkNotDirty(contentValues: ContentValues) {
val id = androidCalendar.addEvent(Entity(
contentValuesOf(
Events.CALENDAR_ID to androidCalendar.id,
Events._ID to 1,
Events.DTSTART to System.currentTimeMillis(),
Events.DTEND to System.currentTimeMillis(),
Events.TITLE to "Some Event",
AndroidEvent2.COLUMN_FLAGS to 123
).apply { putAll(contentValues) }
))
val updated = calendar.markNotDirty(321)
assertEquals(1, updated)
assertEquals(321, androidCalendar.getEvent(id)?.flags)
}
@Test
fun test_markNotDirty_DirtyIs0() = testMarkNotDirty(
contentValuesOf(
Events.DIRTY to 0
)
)
@Test
fun test_markNotDirty_DirtyIsNull() = testMarkNotDirty(
contentValuesOf(
Events.DIRTY to null
)
)
}

View File

@@ -32,7 +32,7 @@ class LocalTestResource: LocalResource<Any> {
this.flags = flags
}
override fun update(data: Any, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) = throw NotImplementedError()
override fun updateFromRemote(data: Any, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) = throw NotImplementedError()
override fun deleteLocal() = throw NotImplementedError()
override fun resetDeleted() = throw NotImplementedError()

View File

@@ -35,13 +35,6 @@ import dagger.hilt.components.SingletonComponent
import java.io.Writer
import javax.inject.Singleton
/**
* The app database. Managed via android jetpack room. Room provides an abstraction
* layer over SQLite.
*
* Note: In SQLite PRAGMA foreign_keys is off by default. Room activates it for
* production (non-test) databases.
*/
@Database(entities = [
Service::class,
HomeSet::class,

View File

@@ -234,7 +234,7 @@ open class LocalAddressBook @AssistedInject constructor(
if (syncInterval != null)
syncFramework.enableSyncOnContentChange(addressBookAccount, ContactsContract.AUTHORITY)
else
syncFramework.disableSyncOnContentChange(addressBookAccount, ContactsContract.AUTHORITY)
syncFramework.disableSyncAbility(addressBookAccount, ContactsContract.AUTHORITY)
}

View File

@@ -10,7 +10,8 @@ import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2
import at.bitfire.synctools.icalendar.AssociatedEvents
import at.bitfire.synctools.mapping.calendar.AndroidEventBuilder
import at.bitfire.synctools.storage.BatchOperation
import at.bitfire.synctools.storage.calendar.AndroidCalendar
import at.bitfire.synctools.storage.calendar.AndroidEvent2
@@ -63,16 +64,17 @@ class LocalCalendar @AssistedInject constructor(
private val recurringCalendar = AndroidRecurringCalendar(androidCalendar)
fun add(event: Event, fileName: String, eTag: String?, scheduleTag: String?, flags: Int) {
val mapped = LegacyAndroidEventBuilder2(
calendar = androidCalendar,
event = event,
id = null,
fun addFromRemote(associatedEvents: AssociatedEvents, legacyEvent: Event, fileName: String, eTag: String?, scheduleTag: String?, flags: Int) {
val mapped = AndroidEventBuilder(
syncId = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = flags
).build()
flags = flags,
associatedEvents = associatedEvents,
androidCalendar = androidCalendar,
event = legacyEvent,
id = null
).build() ?: TODO("Handle invalid event")
recurringCalendar.addEventAndExceptions(mapped)
}
@@ -107,12 +109,7 @@ class LocalCalendar @AssistedInject constructor(
override fun markNotDirty(flags: Int) =
androidCalendar.updateEventRows(
contentValuesOf(AndroidEvent2.COLUMN_FLAGS to flags),
// `dirty` can be 0, 1, or null. "NOT dirty" is not enough.
"""
${Events.CALENDAR_ID}=?
AND (${Events.DIRTY} IS NULL OR ${Events.DIRTY}=0)
AND ${Events.ORIGINAL_ID} IS NULL
""".trimIndent(),
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
arrayOf(androidCalendar.id.toString())
)
@@ -121,20 +118,14 @@ class LocalCalendar @AssistedInject constructor(
val batch = CalendarBatchOperation(androidCalendar.client)
androidCalendar.iterateEventRows(
arrayOf(Events._ID),
// `dirty` can be 0, 1, or null. "NOT dirty" is not enough.
"""
${Events.CALENDAR_ID}=?
AND (${Events.DIRTY} IS NULL OR ${Events.DIRTY}=0)
AND ${Events.ORIGINAL_ID} IS NULL
AND ${AndroidEvent2.COLUMN_FLAGS}=?
""".trimIndent(),
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent2.COLUMN_FLAGS}=?",
arrayOf(androidCalendar.id.toString(), flags.toString())
) { values ->
val id = values.getAsLong(Events._ID)
val id = values.getAsInteger(Events._ID)
// delete event and possible exceptions (content provider doesn't delete exceptions itself)
batch += BatchOperation.CpoBuilder
.newDelete(androidCalendar.eventsUri)
.newDelete(Events.CONTENT_URI.asSyncAdapter(androidCalendar.account))
.withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString()))
}
return batch.commit()

View File

@@ -49,7 +49,8 @@ interface LocalCollection<out T: LocalResource<*>> {
fun findByName(name: String): T?
/**
* Updates the flags value for entries which are not dirty.
* Sets the [LocalEvent.COLUMN_FLAGS] value for entries which are not dirty ([Events.DIRTY] is 0)
* and have an [Events.ORIGINAL_ID] of null.
*
* @param flags value of flags to set (for instance, [LocalResource.FLAG_REMOTELY_PRESENT]])
*
@@ -58,7 +59,8 @@ interface LocalCollection<out T: LocalResource<*>> {
fun markNotDirty(flags: Int): Int
/**
* Removes entries which are not dirty with a given flag combination.
* Removes entries which are not dirty ([Events.DIRTY] is 0 and an [Events.ORIGINAL_ID] is null) with
* a given flag combination.
*
* @param flags exact flags value to remove entries with (for instance, if this is [LocalResource.FLAG_REMOTELY_PRESENT]],
* all entries with exactly this flag will be removed)

View File

@@ -114,7 +114,7 @@ class LocalContact: AndroidContact, LocalAddress {
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
}
override fun update(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
override fun updateFromRemote(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags

View File

@@ -8,7 +8,8 @@ import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.LegacyAndroidCalendar
import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2
import at.bitfire.synctools.icalendar.AssociatedEvents
import at.bitfire.synctools.mapping.calendar.AndroidEventBuilder
import at.bitfire.synctools.storage.LocalStorageException
import at.bitfire.synctools.storage.calendar.AndroidEvent2
import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar
@@ -18,7 +19,7 @@ import java.util.UUID
class LocalEvent(
val recurringCalendar: AndroidRecurringCalendar,
val androidEvent: AndroidEvent2
) : LocalResource<Event> {
) : LocalResource<AssociatedEvents> {
override val id: Long
get() = androidEvent.id
@@ -36,17 +37,21 @@ class LocalEvent(
get() = androidEvent.flags
override fun update(data: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
val eventAndExceptions = LegacyAndroidEventBuilder2(
calendar = androidEvent.calendar,
event = data,
id = id,
syncId = fileName,
override fun updateFromRemote(data: AssociatedEvents, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
val eventAndExceptions = AndroidEventBuilder(
associatedEvents = data,
calendarId = androidEvent.calendar.id,
syncId = fileName ?: TODO(),
eTag = eTag,
scheduleTag = scheduleTag,
flags = flags
).build()
recurringCalendar.updateEventAndExceptions(id, eventAndExceptions)
if (eventAndExceptions != null)
recurringCalendar.updateEventAndExceptions(id, eventAndExceptions)
else {
// TODO handle invalid event
}
}

View File

@@ -212,7 +212,7 @@ class LocalGroup: AndroidGroup, LocalAddress {
batch.commit()
}
override fun update(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
override fun updateFromRemote(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag

View File

@@ -50,7 +50,7 @@ class LocalJtxICalObject(
}
override fun update(data: JtxICalObject, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
override fun updateFromRemote(data: JtxICalObject, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag

View File

@@ -81,7 +81,7 @@ interface LocalResource<in TData: Any> {
*
* @return content URI of the updated row (e.g. event URI)
*/
fun update(data: TData, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int)
fun updateFromRemote(data: TData, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int)
/**
* Deletes the data object from the content provider.

View File

@@ -94,7 +94,7 @@ class LocalTask: DmfsTask, LocalResource<Task> {
this.eTag = eTag
}
override fun update(data: Task, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
override fun updateFromRemote(data: Task, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) {
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag

View File

@@ -354,12 +354,7 @@ class AccountSettings @AssistedInject constructor(
companion object {
/**
* Current (usually the newest) account settings version. It's used to
* determine whether a migration ([AccountSettingsMigration])
* should be performed.
*/
const val CURRENT_VERSION = 21
const val CURRENT_VERSION = 20
const val KEY_SETTINGS_VERSION = "version"
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"

View File

@@ -10,8 +10,7 @@ import at.bitfire.davdroid.settings.AccountSettings
interface AccountSettingsMigration {
/**
* Migrate the account settings from the old version to the new version which
* is set in [AccountSettings.CURRENT_VERSION].
* Migrate the account settings from the old version to the new version.
*
* **The new (target) version number is registered in the Hilt module as [Int] key of the multi-binding of [AccountSettings].**
*

View File

@@ -1,76 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.settings.migration
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import at.bitfire.davdroid.R
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntKey
import dagger.multibindings.IntoMap
import java.util.logging.Logger
import javax.inject.Inject
/**
* On Android 14+ the pending sync state of the Sync Adapter Framework is not handled correctly.
* As a workaround we cancel incoming sync requests (clears pending flag) after enqueuing our own
* sync worker (work manager). With version 4.5.3 we started cancelling pending syncs for DAVx5
* accounts, but forgot to do that for address book accounts. With version 4.5.4 we also cancel
* those, but only when contact data of an address book has been edited.
*
* This migration cancels (once only) any possibly still wrongly pending address book and calendar
* (+tasks) account syncs.
*/
class AccountSettingsMigration21 @Inject constructor(
@ApplicationContext private val context: Context,
private val syncFrameworkIntegration: SyncFrameworkIntegration,
private val logger: Logger
): AccountSettingsMigration {
private val accountManager = AccountManager.get(context)
private val calendarAccountType = context.getString(R.string.account_type)
private val addressBookAccountType = context.getString(R.string.account_type_address_book)
override fun migrate(account: Account) {
if (Build.VERSION.SDK_INT >= 34) {
// Cancel any (after an update) possibly forever pending calendar (+tasks) account syncs
cancelSyncs(calendarAccountType, CalendarContract.AUTHORITY)
// Cancel any (after an update) possibly forever pending address book account syncs
cancelSyncs(addressBookAccountType, ContactsContract.AUTHORITY)
}
}
/**
* Cancels any (possibly forever pending) syncs for the accounts of given account type for all
* authorities.
*/
private fun cancelSyncs(accountType: String, authority: String) {
accountManager.getAccountsByType(accountType).forEach { account ->
logger.info("Android 14+: Canceling all (possibly forever pending) syncs for $account")
syncFrameworkIntegration.cancelSync(account, authority, Bundle())
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AccountSettingsMigrationModule {
@Binds @IntoMap
@IntKey(21)
abstract fun provide(impl: AccountSettingsMigration21): AccountSettingsMigration
}
}

View File

@@ -28,26 +28,23 @@ import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.SyncState
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.EventReader
import at.bitfire.ical4android.EventWriter
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.synctools.exception.InvalidRemoteResourceException
import at.bitfire.synctools.icalendar.CalendarUidSplitter
import at.bitfire.synctools.icalendar.ICalendarParser
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runInterruptible
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.component.VEvent
import okhttp3.HttpUrl
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.Reader
import java.io.StringReader
import java.io.StringWriter
import java.time.Duration
import java.time.ZonedDateTime
import java.util.Optional
import java.util.logging.Level
@@ -262,55 +259,63 @@ class CalendarSyncManager @AssistedInject constructor(
// helpers
private fun processVEvent(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) {
val events: List<Event>
try {
events = EventReader().readEvents(reader)
val vEvent = try {
val iCalendar = ICalendarParser().parse(reader)
val vEvents = CalendarUidSplitter<VEvent>().associateByUid(iCalendar, Component.VEVENT)
if (vEvents.size > 1)
logger.warning("Received iCalendar with more than one UID; ignoring all but first one")
vEvents.values.firstOrNull()
} catch (e: InvalidRemoteResourceException) {
logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
notifyInvalidResource(e, fileName)
return
}
if (events.size == 1) {
val event = events.first()
if (vEvent == null) {
logger.warning("Ignoring iCalendar without VEVENTs ($fileName)")
return
}
// set default reminder for non-full-day events, if requested
val defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) {
val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply {
// Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider.
// Needed for calendars to actually show a notification.
properties += Action.DISPLAY
}
logger.log(Level.FINE, "${event.uid}: Adding default alarm", alarm)
event.alarms += alarm
}
// TODO we need both the new associated event and the legacy event here,
// then pass both to addupdateFromRemote / updateFromRemote.
// update local event, if it exists
val local = localCollection.findByName(fileName)
SyncException.wrapWithLocalResource(local) {
if (local != null) {
logger.log(Level.INFO, "Updating $fileName in local calendar", event)
local.update(
data = event,
fileName = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
} else {
logger.log(Level.INFO, "Adding $fileName to local calendar", event)
localCollection.add(
event = event,
fileName = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
}
// TODO add default reminder
/* set default reminder for non-full-day events, if requested
val defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) {
val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply {
// Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider.
// Needed for calendars to actually show a notification.
properties += Action.DISPLAY
}
} else
logger.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
logger.log(Level.FINE, "${event.uid}: Adding default alarm", alarm)
event.alarms += alarm
} */
val localEvent = localCollection.findByName(fileName)
SyncException.wrapWithLocalResource(localEvent) {
if (localEvent != null) {
logger.log(Level.INFO, "Updating $fileName in local calendar", vEvent)
localEvent.updateFromRemote(
data = vEvent,
fileName = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
} else {
logger.log(Level.INFO, "Adding $fileName to local calendar", vEvent)
localCollection.addFromRemote(
associatedEvents = vEvent,
fileName = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
}
}
}
override fun notifyInvalidResourceTitle(): String =

View File

@@ -421,7 +421,7 @@ class ContactsSyncManager @AssistedInject constructor(
if ((existing is LocalGroup && newData.group) || (existing is LocalContact && !newData.group)) {
// update contact / group
existing.update(
existing.updateFromRemote(
data = newData,
fileName = fileName,
eTag = eTag,

View File

@@ -8,7 +8,6 @@ import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.content.SyncRequest
import android.os.Build
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.resource.LocalAddressBookStore
@@ -101,9 +100,11 @@ class SyncFrameworkIntegration @Inject constructor(
}
/**
* Cancels the sync request in the Sync Adapter Framework by sync request. This
* is the defensive approach canceling only one specific sync request with matching
* sync extras.
* Cancels the sync request in the Sync Framework for Android 14+.
* This is a workaround for the bug that the sync framework does not handle pending syncs correctly
* on Android 14+ (API level 34+).
*
* See: https://github.com/bitfireAT/davx5-ose/issues/1458
*
* @param account The account for which the sync request should be canceled.
* @param authority The authority for which the sync request should be canceled.

View File

@@ -5,8 +5,6 @@
package at.bitfire.davdroid.ui
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@@ -15,7 +13,6 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.PowerManager
import android.provider.CalendarContract
import android.provider.ContactsContract
@@ -25,7 +22,6 @@ import androidx.lifecycle.ViewModel
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
@@ -300,34 +296,4 @@ class AccountsModel @AssistedInject constructor(
false
}
fun cancelSyncAdapterSyncs() {
if (Build.VERSION.SDK_INT >= 34) {
val calendarAccountType = context.getString(R.string.account_type)
val addressBookAccountType = context.getString(R.string.account_type_address_book)
// Cancel any (after an update) possibly forever pending calendar account syncs
cancelSyncs(calendarAccountType, SyncDataType.EVENTS.possibleAuthorities())
// Cancel any (after an update) possibly forever pending tasks account syncs
cancelSyncs(calendarAccountType, SyncDataType.TASKS.possibleAuthorities())
// Cancel any (after an update) possibly forever pending address book account syncs
cancelSyncs(addressBookAccountType, SyncDataType.CONTACTS.possibleAuthorities())
}
}
/**
* Cancels any (possibly forever pending) syncs for the accounts of given account type for all
* authorities.
*/
private fun cancelSyncs(accountType: String, authorities: List<String>) {
val accountManager = AccountManager.get(context)
accountManager.getAccountsByType(accountType).forEach { account ->
logger.info("Android 14+: Canceling all (possibly forever pending) syncs for $account")
for (authority in authorities)
ContentResolver.cancelSync(account, authority)
}
}
}
}

View File

@@ -23,7 +23,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BatterySaver
import androidx.compose.material.icons.filled.CancelScheduleSend
import androidx.compose.material.icons.filled.DataSaverOn
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.NotificationsOff
@@ -112,7 +111,6 @@ fun AccountsScreen(
}
AccountsScreen(
cancelSyncAdapterSyncs = { model.cancelSyncAdapterSyncs() },
accountsDrawerHandler = accountsDrawerHandler,
accounts = accounts,
showSyncAll = showSyncAll,
@@ -133,7 +131,6 @@ fun AccountsScreen(
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun AccountsScreen(
cancelSyncAdapterSyncs: () -> Unit,
accountsDrawerHandler: AccountsDrawerHandler,
accounts: List<AccountsModel.AccountInfo>,
showSyncAll: Boolean = true,
@@ -231,17 +228,6 @@ fun AccountsScreen(
contentDescription = stringResource(R.string.accounts_sync_all)
)
}
FloatingActionButton(
onClick = cancelSyncAdapterSyncs,
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier.padding(top = 24.dp)
) {
Icon(
Icons.Default.CancelScheduleSend,
contentDescription = stringResource(R.string.accounts_sync_all)
)
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) }
@@ -335,7 +321,6 @@ fun AccountsScreen(
@Preview
fun AccountsScreen_Preview_Empty() {
AccountsScreen(
cancelSyncAdapterSyncs = {},
accountsDrawerHandler = object: AccountsDrawerHandler() {
@Composable
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
@@ -352,7 +337,6 @@ fun AccountsScreen_Preview_Empty() {
@Preview
fun AccountsScreen_Preview_OneAccount() {
AccountsScreen(
cancelSyncAdapterSyncs = {},
accountsDrawerHandler = object: AccountsDrawerHandler() {
@Composable
override fun MenuEntries(snackbarHostState: SnackbarHostState) {

View File

@@ -26,8 +26,6 @@ import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.CreateNewFolder
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.DriveFileRenameOutline
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
@@ -359,7 +357,7 @@ fun AccountScreen(
AccountScreen_Tab(
selected = idxCurrentPage == idxCardDav,
showIcon = showIcon,
icon = Icons.Default.Group,
icon = Icons.Default.Person,
text = stringResource(R.string.account_carddav),
animatedVisibilityScope = this@AnimatedContent,
sharedTransitionScope = this@SharedTransitionLayout,
@@ -373,7 +371,7 @@ fun AccountScreen(
AccountScreen_Tab(
selected = idxCurrentPage == idxWebcal,
showIcon = showIcon,
icon = Icons.Default.Link,
icon = Icons.Default.CalendarImport,
text = stringResource(R.string.account_webcal),
animatedVisibilityScope = this@AnimatedContent,
sharedTransitionScope = this@SharedTransitionLayout,

View File

@@ -204,7 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Отдалечено подадените съобщения са винаги шифровани.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Профилът е премахнат</string>
<string name="account_invalid_account">Регистрацията не съществува</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -204,7 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Els missatges «push» sempre són xifrats.</string>
<!--AccountScreen-->
<string name="account_invalid_account">S\'ha eliminat el compte</string>
<string name="account_invalid_account">El compte no existeix</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -246,10 +246,8 @@
<string name="login_password">Contrasenya</string>
<string name="login_password_hide">Oculta la contrasenya</string>
<string name="login_password_show">Mostra la contrasenya</string>
<string name="login_password_optional">Contrasenya (opcional)</string>
<string name="login_type_url">Inici de sessió amb un URL i un nom d\'usuari/ària</string>
<string name="login_user_name">Nom d\'usuari/ària</string>
<string name="login_user_name_optional">Nom d\'usuari (opcional)</string>
<string name="login_base_url">URL base</string>
<string name="login_base_url_info"><![CDATA[L\'URL base es comprovarà directament, però els <a href="%s">serveis també es descobreixen</a> utilitzant els registres de DNS i els URL ben coneguts.]]></string>
<string name="login_select_certificate">Selecciona el certificat</string>
@@ -263,7 +261,6 @@
<string name="login_account_not_added">No s\'ha pogut afegir el compte</string>
<string name="login_finish">Finalitza</string>
<string name="login_type_advanced">Inici de sessió avançat</string>
<string name="login_no_client_certificate_optional">Sense certificat del client (opcional)</string>
<string name="login_client_certificate_selected">Certificat del client: %s</string>
<string name="login_no_certificate_found">No s\'ha trobat cap certificat</string>
<string name="login_install_certificate">Instal·la un certificat</string>
@@ -363,7 +360,6 @@
<string name="create_addressbook">Crea una llibreta d\'adreces</string>
<string name="create_addressbook_maybe_not_supported">La creació de llibretes d\'adreces sobre CardDAV pot no ser suportada pel servidor.</string>
<string name="create_calendar">Crea un calendari</string>
<string name="create_calendar_time_zone_optional">Fus horari predeterminat (opcional)</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Possibles entrades de calendari</string>
<string name="create_calendar_type_vevent">Esdeveniments</string>
@@ -439,12 +435,9 @@
<string name="webdav_add_mount_display_name">Nom a mostrar</string>
<string name="webdav_add_mount_url">URL del WebDAV</string>
<string name="webdav_add_mount_url_invalid">URL no vàlid</string>
<string name="webdav_add_mount_mountpoint_displayname">Punt de muntatge i nom de visualització</string>
<string name="webdav_add_mount_authentication">Autentificació</string>
<string name="webdav_add_mount_username">Nom d\'usuari/ària</string>
<string name="webdav_add_mount_password">Contrasenya</string>
<string name="webdav_add_mount_username_optional">Nom d\'usuari (opcional)</string>
<string name="webdav_add_mount_password_optional">Contrasenya (opcional)</string>
<string name="webdav_add_mount_add">Afegeix un muntatge</string>
<string name="webdav_add_mount_no_support">No hi ha cap servei WebDAV en aquest URL</string>
<string name="webdav_remove_mount_title">Elimina el punt de muntatge</string>

View File

@@ -204,7 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Push-Nachrichten sind immer verschlüsselt.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Konto wurde entfernt</string>
<string name="account_invalid_account">Konto nicht vorhanden</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -204,7 +204,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Tõuketeavituste sõnumid on alati krüptitud.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Kasutajakonto on eemaldatud</string>
<string name="account_invalid_account">Kasutajakontot pole olemas</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -33,7 +33,6 @@
<string name="intro_battery_text">Sinkronizazioa tarte erregularretan ahalbidetzeko, %s atzeko planoan exekutatzen utzi behar da. Bestela, Androidek sinkronizazioa gelditu dezake edozein unean.</string>
<string name="intro_battery_dont_show">Ez ditut sinkronizazio tarte erregularrak behar.*</string>
<string name="intro_autostart_title">%s bateragarritasuna</string>
<string name="intro_autostart_text">Salatzailearen firmwareak sinkronizazioa blokeatu dezake. Kaltetua bazara, eskuz bakarrik konpon dezakezu.</string>
<string name="intro_autostart_dont_show">Beharrezko ezarpenak bukatu ditut. Ez gogorarazi berriro.*</string>
<string name="intro_leave_unchecked">* Utzi aktibatu gabe gero gogorarazteko. Aplikazioaren ezarpenetan berrezarri daiteke / %s</string>
<string name="intro_more_info">Informazio gehiago</string>
@@ -121,7 +120,6 @@
<string name="navigation_drawer_website">Webgunea</string>
<string name="navigation_drawer_manual">Manuala</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_managed">Erakundeentzat</string>
<string name="navigation_drawer_community">Komunitatea</string>
<string name="navigation_drawer_support_project">Lagundu proiektuari</string>
<string name="navigation_drawer_contribute">Nola lagundu</string>
@@ -205,7 +203,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Bultzatutako mezuak beti zifratzen dira.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Kontua ezabatu da</string>
<string name="account_invalid_account">Kontua ez da existitzen</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -247,10 +245,8 @@
<string name="login_password">Pasahitza</string>
<string name="login_password_hide">Ezkutatu pasahitza</string>
<string name="login_password_show">Erakutsi pasahitza</string>
<string name="login_password_optional">Pasahitza (aukerakoa)</string>
<string name="login_type_url">Saioa hasi URL eta erabiltzaile izenarekin</string>
<string name="login_user_name">Erabiltzaile izena</string>
<string name="login_user_name_optional">Erabiltzaile izena (aukerakoa)</string>
<string name="login_base_url">Oinarri URL</string>
<string name="login_base_url_info"><![CDATA[Oinarrizko URLa zuzenean egiaztatuko da, baina <a href="%s">zerbitzuak ere aurkitzen dira</a> DNS erregistroak eta URL ezagunak erabilita.]]></string>
<string name="login_select_certificate">Aukeratu ziurtagiria</string>
@@ -264,7 +260,6 @@
<string name="login_account_not_added">Ezin izan da kontua gehitu</string>
<string name="login_finish">Bukatu</string>
<string name="login_type_advanced">Saio-hasiera aurreratua</string>
<string name="login_no_client_certificate_optional">Bezero-ziurtagiririk gabe (aukerakoa)</string>
<string name="login_client_certificate_selected">Bezeroaren ziurtagiria: %s</string>
<string name="login_no_certificate_found">Ez da ziurtagiririk aurkitu</string>
<string name="login_install_certificate">Instalatu ziurtagiria</string>
@@ -364,7 +359,6 @@
<string name="create_addressbook">Sortu helbide liburua</string>
<string name="create_addressbook_maybe_not_supported">Baliteke zerbitzariak ez onartzea CardDAV bidez sortutako helbide-liburua.</string>
<string name="create_calendar">Sortu egutegia</string>
<string name="create_calendar_time_zone_optional">Lehenetsitako ordu-zona (aukerakoa)</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Egutegi sarrera posibleak</string>
<string name="create_calendar_type_vevent">Gertaerak</string>
@@ -440,12 +434,9 @@
<string name="webdav_add_mount_display_name">Bistaratze-izena</string>
<string name="webdav_add_mount_url">WebDAV URL</string>
<string name="webdav_add_mount_url_invalid">URL baliogabea</string>
<string name="webdav_add_mount_mountpoint_displayname">Muntatu puntua eta bistaratu izena</string>
<string name="webdav_add_mount_authentication">Autentifikazioa</string>
<string name="webdav_add_mount_username">Erabiltzaile izena</string>
<string name="webdav_add_mount_password">Pasahitza</string>
<string name="webdav_add_mount_username_optional">Erabiltzaile izena (aukerakoa)</string>
<string name="webdav_add_mount_password_optional">Pasahitza (aukerakoa)</string>
<string name="webdav_add_mount_add">Gehitu muntaia</string>
<string name="webdav_add_mount_no_support">Ez dago WebDAV zerbitzurik URL honetan</string>
<string name="webdav_remove_mount_title">Kendu muntaia-puntua</string>

View File

@@ -203,7 +203,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">プッシュメッセージは常に暗号化されます。</string>
<!--AccountScreen-->
<string name="account_invalid_account">アカウントが削除されました</string>
<string name="account_invalid_account">アカウントが存在しません</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -201,6 +201,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">푸시 메시지는 항상 암호화됩니다.</string>
<!--AccountScreen-->
<string name="account_invalid_account">계정이 존재하지 않습니다.</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -5,8 +5,8 @@
<string name="account_title_address_book">DAVx⁵ Adresboek</string>
<string name="account_prefs_use_app">Verander hier niet van account! Gebruik in plaats daarvan direct de app om accounts te beheren.</string>
<string name="dialog_delete">Verwijderen</string>
<string name="dialog_remove">Verwijderen</string>
<string name="dialog_deny">Annuleren</string>
<string name="dialog_remove">Verwijder</string>
<string name="dialog_deny">Annuleer</string>
<string name="dialog_enable">Inschakelen</string>
<string name="field_required">Dit veld is verplicht</string>
<string name="help">Hulp</string>
@@ -161,14 +161,14 @@
<string name="app_settings_battery_optimization_exempted">App is vrijgesteld (aanbevolen)</string>
<string name="app_settings_battery_optimization_optimized">Batterijbeperkingen van toepassing (niet aanbevolen)</string>
<string name="app_settings_connection">Verbinding</string>
<string name="app_settings_proxy">Proxy-type</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">
<item>Systeem standaard</item>
<item>Geen proxy</item>
<item>HTTP</item>
<item>SOCKS (voor Orbot)</item>
</string-array>
<string name="app_settings_proxy_host">Proxy hostnaam</string>
<string name="app_settings_proxy_host">Proxy host naam</string>
<string name="app_settings_proxy_port">Proxy poort</string>
<string name="app_settings_security">Beveiliging</string>
<string name="app_settings_security_app_permissions">App rechten</string>
@@ -204,20 +204,20 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Pushberichten zijn altijd versleuteld.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Account is verwijderd</string>
<string name="account_invalid_account">Account bestaat niet</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">Er zijn extra rechten nodig om deze collecties te synchroniseren.</string>
<string name="account_manage_permissions">Machtigingen beheren</string>
<string name="account_synchronize_now">Nu synchroniseren</string>
<string name="account_settings">Account-instellingen</string>
<string name="account_rename">Accountnaam wijzigen</string>
<string name="account_settings">Account instellingen</string>
<string name="account_rename">Naam account wijzigen</string>
<string name="account_rename_new_name_description">Niet opgeslagen lokale gegevens kunnen worden verwijderd. Na het hernoemen is opnieuw synchroniseren vereist.</string>
<string name="account_rename_new_name">Nieuwe accountnaam</string>
<string name="account_rename_rename">Naam wijzigen</string>
<string name="account_rename_exists_already">Accountnaam is al in gebruik</string>
<string name="account_rename_couldnt_rename">Accountnaam is niet gewijzigd</string>
<string name="account_rename_couldnt_rename">Naam account is niet gewijzigd</string>
<string name="account_delete">Account verwijderen</string>
<string name="account_delete_confirmation_title">Account echt verwijderen?</string>
<string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, kalenders en takenlijsten worden verwijderd.</string>
@@ -244,7 +244,7 @@
<string name="login_email_address_error">Geldig e-mailadres vereist</string>
<string name="login_email_address_info"><![CDATA[Het e-maildomein wordt gebruikt als basis-URL. <a href="%s">Diensten worden ontdekt</a> met behulp van DNS-records en bekende URL\'s.]]></string>
<string name="login_password">Wachtwoord</string>
<string name="login_password_hide">Wachtwoord verbergen</string>
<string name="login_password_hide">Verberg wachtwoord</string>
<string name="login_password_show">Wachtwoord tonen</string>
<string name="login_password_optional">Wachtwoord (optioneel)</string>
<string name="login_type_url">Inloggen met URL en gebruikersnaam</string>
@@ -261,14 +261,14 @@
<string name="login_account_name_required">Accountnaam verplicht</string>
<string name="login_account_name_already_taken">Accountnaam is al in gebruik</string>
<string name="login_account_not_added">Account kon niet worden toegevoegd</string>
<string name="login_finish">Afwerken</string>
<string name="login_finish">afwerken</string>
<string name="login_type_advanced">Geavanceerd inloggen</string>
<string name="login_no_client_certificate_optional">Geen cliëntcertificaat (optioneel)</string>
<string name="login_client_certificate_selected">Cliëntcertificaat: %s</string>
<string name="login_no_certificate_found">Geen certificaat gevonden</string>
<string name="login_install_certificate">Certificaat installeren</string>
<string name="login_fastmail">Fastmail</string>
<string name="login_fastmail_account">Fastmail-account</string>
<string name="login_fastmail_account">Fastmail account</string>
<string name="login_fastmail_sign_in">Inloggen met Fastmail</string>
<string name="login_type_google">Google Contacten / Kalender</string>
<string name="login_google_account">Google account</string>
@@ -395,7 +395,7 @@
<string name="collection_push_support">Push-ondersteuning</string>
<string name="collection_push_web_push">Server adverteert Push-ondersteuning</string>
<string name="collection_push_subscribed_at">Ingeschreven op %1$s, vervalt op %2$s</string>
<string name="collection_last_sync">Laatste synchronisatie (%s)</string>
<string name="collection_last_sync">Laatste gesynchroniseerd (%s)</string>
<string name="collection_url">Adres (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Debug informatie</string>

View File

@@ -33,7 +33,6 @@
<string name="intro_battery_text">Pentru sincronizare la intervale regulate, %s trebuie să aibă voie să ruleze în fundal. În caz contrar, Android poate întrerupe sincronizarea în orice moment.</string>
<string name="intro_battery_dont_show">Nu am nevoie de intervale regulate de sincronizare.*</string>
<string name="intro_autostart_title">Compatibilitate %s </string>
<string name="intro_autostart_text">Firmware-ul specific vendorului poate bloca sincronizarea. Dacă ești afectat, poți rezolva acest lucru manual.</string>
<string name="intro_autostart_dont_show">Am făcut setările necesare. Nu-mi mai aminti.*</string>
<string name="intro_leave_unchecked">* Lasă nebifat pentru a fi reamintit mai târziu. Poate fi resetat în setările aplicației / %s.</string>
<string name="intro_more_info">Mai multe informații</string>
@@ -121,7 +120,6 @@
<string name="navigation_drawer_website">Pagină web</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">Întrebări frecvente</string>
<string name="navigation_drawer_managed">Pentru organizații</string>
<string name="navigation_drawer_community">Comunitate</string>
<string name="navigation_drawer_support_project">Susține proiectul</string>
<string name="navigation_drawer_contribute">Cum să contribui</string>
@@ -205,7 +203,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Mesajele push sunt întotdeauna criptate.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Contul a fost eliminat</string>
<string name="account_invalid_account">Contul nu există</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -247,10 +245,8 @@
<string name="login_password">Parolă</string>
<string name="login_password_hide">Ascunde parola</string>
<string name="login_password_show">Afișează parola</string>
<string name="login_password_optional">Parolă (opțional)</string>
<string name="login_type_url">Conecteează-te cu adresa URL și numele de utilizator</string>
<string name="login_user_name">Nume de utilizator</string>
<string name="login_user_name_optional">Nume de utilizator (opțional)</string>
<string name="login_base_url">Adresa URL de bază</string>
<string name="login_base_url_info"><![CDATA[Adresa URL de bază va fi verificată direct, dar <a href="%s">serviciile sunt de asemenea descoperite</a> folosind înregistrări DNS și adrese URL bine-cunoscute.]]></string>
<string name="login_select_certificate">Selectează certificatul</string>
@@ -264,7 +260,6 @@
<string name="login_account_not_added">Contul nu a putut fi adăugat</string>
<string name="login_finish">Finalizează</string>
<string name="login_type_advanced">Autentificare avansată</string>
<string name="login_no_client_certificate_optional">Fără certificat de client (opțional)</string>
<string name="login_client_certificate_selected">Certificat de client: %s</string>
<string name="login_no_certificate_found">Nu a fost găsit niciun certificat</string>
<string name="login_install_certificate">Instalare certificat</string>
@@ -366,7 +361,6 @@
<string name="create_addressbook">Creează agendă de adrese</string>
<string name="create_addressbook_maybe_not_supported">Crearea agendei prin CardDAV poate să nu fie acceptată de server.</string>
<string name="create_calendar">Creează un calendar</string>
<string name="create_calendar_time_zone_optional">Fus orar implicit (opțional)</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Posibile intrări din calendar</string>
<string name="create_calendar_type_vevent">Evenimente</string>
@@ -442,12 +436,9 @@
<string name="webdav_add_mount_display_name">Numele afișat</string>
<string name="webdav_add_mount_url">URL WebDAV</string>
<string name="webdav_add_mount_url_invalid">URL greșit</string>
<string name="webdav_add_mount_mountpoint_displayname">Punctul de montare și numele de afișare</string>
<string name="webdav_add_mount_authentication">Autentificare</string>
<string name="webdav_add_mount_username">Nume de utilizator</string>
<string name="webdav_add_mount_password">Parolă</string>
<string name="webdav_add_mount_username_optional">Nume de utilizator (opțional)</string>
<string name="webdav_add_mount_password_optional">Parolă (opțional)</string>
<string name="webdav_add_mount_add">Adaugă montare</string>
<string name="webdav_add_mount_no_support">Niciun serviciu WebDAV la această adresă URL</string>
<string name="webdav_remove_mount_title">Elimină punctul de montare</string>

View File

@@ -206,7 +206,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">Push-сообщения всегда зашифрованы.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Аккаунт удален</string>
<string name="account_invalid_account">Аккаунт не существует</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
@@ -417,10 +417,10 @@
<string name="debug_info_http_403_description">Запрос был отклонен. Для получения подробной информации проверьте задействованные ресурсы и отладочную информацию.</string>
<string name="debug_info_http_404_description">Запрошенного ресурса не существует (больше не существует). Проверьте задействованные ресурсы и отладочную информацию для получения подробной информации.</string>
<string name="debug_info_http_5xx_description">Возникла проблема на стороне сервера. Пожалуйста, свяжитесь со службой поддержки вашего сервера.</string>
<string name="debug_info_unexpected_error">Произошла неожиданная ошибка. Просмотрите отладочную информацию, чтобы узнать подробности.</string>
<string name="debug_info_view_details">Просмотр</string>
<string name="debug_info_subtitle">Отладочная информация собрана</string>
<string name="debug_info_involved_caption">Задействованные ресурсы</string>
<string name="debug_info_unexpected_error">Произошла неожиданная ошибка. Просмотрите отладочную информацию для получения подробностей.</string>
<string name="debug_info_view_details">Просмотреть информацию</string>
<string name="debug_info_subtitle">Собрана отладочная информация</string>
<string name="debug_info_involved_caption">Вовлеченные ресурсы</string>
<string name="debug_info_involved_subtitle">Связанная с этим проблема</string>
<string name="debug_info_involved_remote">Удаленный ресурс:</string>
<string name="debug_info_involved_local">Локальный ресурс:</string>
@@ -428,7 +428,7 @@
<string name="debug_info_logs_subtitle">Доступны подробные логи</string>
<string name="debug_info_logs_view">Просмотр логов</string>
<string name="debug_info_privacy_warning_title"> Предупреждение о конфиденциальности</string>
<string name="debug_info_privacy_warning_description">Журналы и отладочная информация могут содержать конфиденциальную информацию. Пожалуйста, помните об этом, когда делитесь ими.</string>
<string name="debug_info_privacy_warning_description">Журналы и отладочная информация могут содержать конфиденциальную информацию. Пожалуйста, помните об этом при публичном использовании.</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>
@@ -440,7 +440,7 @@
<string name="webdav_mounts_share_content">Поделиться контентом</string>
<string name="webdav_mounts_unmount">Отмонтировать</string>
<string name="webdav_add_mount_title">Добавление точки монтирования WebDAV</string>
<string name="webdav_mounts_empty">Прямой доступ к вашим облачным файлам с помощью точек монтирования WebDAV!</string>
<string name="webdav_mounts_empty">Прямой доступ к вашим облачным файлам с помощью точки монтирования WebDAV!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[Ознакомьтесь с руководством, чтобы узнать <a href="%1$s">как работают точки монтирования WebDAV</a>.]]></string>
<string name="webdav_add_mount_display_name">Отображаемое имя</string>
<string name="webdav_add_mount_url">WebDAV URL</string>

View File

@@ -201,6 +201,7 @@
<string name="app_settings_unifiedpush_ready">Redo att ta emot push meddelanden över %s</string>
<string name="app_settings_unifiedpush_encrypted">Push-meddelanden är alltid krypterade.</string>
<!--AccountScreen-->
<string name="account_invalid_account">Kontot finns inte</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -3,15 +3,10 @@
<!--common strings-->
<string name="account_invalid">帳號(已)不存在</string>
<string name="account_title_address_book">DAVx⁵ 通訊錄</string>
<string name="account_prefs_use_app">別在這裡更改帳戶!請直接使用應用程式管理帳戶。</string>
<string name="dialog_delete">刪除</string>
<string name="dialog_remove">移除</string>
<string name="dialog_deny">取消</string>
<string name="dialog_enable">啟用</string>
<string name="field_required">此為必填欄位</string>
<string name="help">幫助</string>
<string name="navigate_up">向上導航</string>
<string name="options_menu">選項選單</string>
<string name="share">分享</string>
<string name="sync_started">同步已開始或排入佇列</string>
<string name="database_destructive_migration_title">資料庫損毀</string>
@@ -33,7 +28,6 @@
<string name="intro_battery_text">為了定期進行同步,必須允許 %s 在背景運行,否則 Android 可能會隨時暫停同步。</string>
<string name="intro_battery_dont_show">我不需要定期同步間隔*</string>
<string name="intro_autostart_title">%s 相容性</string>
<string name="intro_autostart_text">特定廠商的韌體可能會阻止同步。如果您受到影響,您只能手動解決這一問題。</string>
<string name="intro_autostart_dont_show">所需設定已完成,不用再提醒我*</string>
<string name="intro_leave_unchecked">* 取消勾選則稍後會再次提醒,可於設定中重置 / %s</string>
<string name="intro_more_info">更多資訊</string>
@@ -41,40 +35,13 @@
<string name="intro_tasks_title">待辦事項支援</string>
<string name="intro_tasks_text1">如果你的服務器支持任務它們可以與支援任務的app同步:</string>
<string name="intro_tasks_opentasks_info">似乎已不再繼續開發 - 不建議使用。</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[某些功能 <a href="https://www.davx5.com/faq/tasks/advanced-task-features">不被支援</a>。]]></string>
<string name="intro_tasks_no_app_store">沒有應用商店可用</string>
<string name="intro_tasks_dont_show">我不需要任務支援。*</string>
<string name="intro_open_source_title">開源軟體</string>
<string name="intro_open_source_text">我們很高興您使用 %s 開源軟體。開發、維護和支持是艱苦的工作。請考慮透過多種方式提供貢獻或捐款。不勝感激!</string>
<string name="intro_open_source_details">如何貢獻或捐款</string>
<string name="intro_open_source_dont_show">不要提醒時長</string>
<plurals name="intro_open_source_dont_show_months">
<item quantity="other">%d 個月</item>
</plurals>
<string name="intro_next">繼續</string>
<!--PermissionsActivity-->
<string name="permissions_title">權限</string>
<string name="permissions_text">%s需要權限才能正常工作</string>
<string name="permissions_all_title">以下所有</string>
<string name="permissions_all_status_off">使用它來啟用所有功能(推薦)</string>
<string name="permissions_all_status_on">已授予所有權限</string>
<string name="permissions_contacts_title">通訊錄權限</string>
<string name="permissions_contacts_status_off">無聯絡人同步(不推薦)</string>
<string name="permissions_contacts_status_on">可同步聯絡人</string>
<string name="permissions_calendar_title">行事曆權限</string>
<string name="permissions_calendar_status_off">無日曆同步(不推薦)</string>
<string name="permissions_calendar_status_on">可同步日曆</string>
<string name="permissions_notification_title">通知權限</string>
<string name="permissions_notification_status_off">已關閉通知(不推薦)</string>
<string name="permissions_notification_status_on">已啟用通知</string>
<string name="permissions_jtx_title">jtx Board 權限</string>
<string name="permissions_opentasks_title">OpenTasks 權限</string>
<string name="permissions_tasksorg_title">Tasks 權限</string>
<string name="permissions_tasks_status_off">無任務同步</string>
<string name="permissions_tasks_status_on">可同步任步</string>
<string name="permissions_autoreset_title">保持權限</string>
<string name="permissions_autoreset_status_off">權限可能會被自動重設(不推薦)</string>
<string name="permissions_autoreset_status_on">權限不會被自動重設</string>
<!--WifiPermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">翻譯</string>

View File

@@ -203,7 +203,7 @@
<string name="app_settings_unifiedpush_distributor_fcm">FCM (Google Play)</string>
<string name="app_settings_unifiedpush_encrypted">推送消息始终是加密的</string>
<!--AccountScreen-->
<string name="account_invalid_account">账户已被删除</string>
<string name="account_invalid_account">账户不存在</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -238,7 +238,7 @@
<string name="app_settings_unifiedpush_encrypted">Push messages are always encrypted.</string>
<!-- AccountScreen -->
<string name="account_invalid_account">Account has been removed</string>
<string name="account_invalid_account">Account doesn\'t exist</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>

View File

@@ -1,13 +1,13 @@
# Comments apply to next line
[versions]
android-agp = "8.12.1"
android-agp = "8.12.0"
android-desugaring = "2.1.5"
androidx-activityCompose = "1.10.1"
androidx-appcompat = "1.7.1"
androidx-arch = "2.2.0"
androidx-browser = "1.9.0"
androidx-core = "1.17.0"
androidx-core = "1.16.0"
androidx-hilt = "1.2.0"
androidx-lifecycle = "2.9.2"
androidx-paging = "3.3.6"
@@ -20,18 +20,18 @@ androidx-test-junit = "1.3.0"
androidx-work = "2.10.3"
bitfire-cert4android = "41009d48ed"
bitfire-dav4jvm = "cb6065b262"
bitfire-synctools = "f12d9a4982"
bitfire-synctools = "4d847edf27"
compose-accompanist = "0.37.3"
compose-bom = "2025.08.00"
compose-bom = "2025.07.00"
dnsjava = "3.6.3"
glance = "1.1.1"
guava = "33.4.8-android"
hilt = "2.57"
# keep in sync with ksp version
kotlin = "2.2.10"
kotlin = "2.2.0"
kotlinx-coroutines = "1.10.2"
# see https://github.com/google/ksp/releases for version numbers
ksp = "2.2.10-2.0.2"
ksp = "2.2.0-2.0.2"
mikepenz-aboutLibraries = "12.2.4"
mockk = "1.14.5"
okhttp = "5.1.0"