mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-02 11:57:50 -05:00
Compare commits
2 Commits
testing-sy
...
synctools-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a89483d8c7 | ||
|
|
60eba44541 |
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -8,7 +8,4 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "[CI] "
|
||||
groups:
|
||||
ci-actions:
|
||||
patterns: ["*"]
|
||||
prefix: "[CI] "
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/test-dev.yml
vendored
12
.github/workflows/test-dev.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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].**
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user