Refactor DavService to WorkManager (bitfireAT/#164)

* SyncManager: remove retry intent
* refactor DavService to RefreshCollectionsWorker now using WorkManager
* move DavResourceFinder to new service detection package
* Optimize imports

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Sunik Kupfer 2022-12-03 14:00:38 +01:00 committed by Ricki Hirner
parent 3f2fcda6d3
commit 02885947da
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
54 changed files with 535 additions and 629 deletions

View file

@ -113,15 +113,19 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.5.4'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha04'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'com.google.android.material:material:1.7.0'
@ -160,6 +164,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.5.1'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
androidTestImplementation 'androidx.work:work-testing:2.7.1'
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
androidTestImplementation 'io.mockk:mockk-android:1.13.2'
androidTestImplementation 'junit:junit:4.13.2'

View file

@ -13,8 +13,9 @@ import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo
import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.mockwebserver.Dispatcher

View file

@ -50,7 +50,13 @@
tools:ignore="UnusedAttribute"
android:supportsRtl="true">
<service android:name=".DavService"/>
<!-- required for Hilt/WorkManager integration -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<service android:name=".ForegroundService"/>
<activity android:name=".ui.intro.IntroActivity" android:theme="@style/AppTheme.NoActionBar" />

View file

@ -10,6 +10,8 @@ import android.net.Uri
import android.os.StrictMode
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.toBitmap
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
@ -24,7 +26,7 @@ import kotlin.concurrent.thread
import kotlin.system.exitProcess
@HiltAndroidApp
class App: Application(), Thread.UncaughtExceptionHandler {
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
companion object {
@ -42,7 +44,7 @@ class App: Application(), Thread.UncaughtExceptionHandler {
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
@Inject lateinit var storageLowReceiver: StorageLowReceiver
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
@ -92,6 +94,11 @@ class App: Application(), Thread.UncaughtExceptionHandler {
}
}
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
override fun uncaughtException(t: Thread, e: Throwable) {
Logger.log.log(Level.SEVERE, "Unhandled exception!", e)

View file

@ -1,431 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.accounts.Account
import android.app.IntentService
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Bundle
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.db.*
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
import javax.inject.Inject
import kotlin.collections.*
@Suppress("DEPRECATION")
@AndroidEntryPoint
class DavService: IntentService("DavService") {
companion object {
const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
const val EXTRA_DAV_SERVICE_ID = "davServiceID"
/** Initialize a forced synchronization. Expects intent data
to be an URI of this format:
contents://<authority>/<account.type>/<account name>
**/
const val ACTION_FORCE_SYNC = "forceSync"
val DAV_COLLECTION_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
Owner.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
fun refreshCollections(context: Context, serviceId: Long) {
val intent = Intent(context, DavService::class.java)
intent.action = DavService.ACTION_REFRESH_COLLECTIONS
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceId)
context.startService(intent)
}
}
@Inject lateinit var db: AppDatabase
@Inject lateinit var settings: SettingsManager
/**
* List of [Service] IDs for which the collections are currently refreshed
*/
private val runningRefresh = Collections.synchronizedSet(HashSet<Long>())
/**
* Currently registered [RefreshingStatusListener]s, which will be notified
* when a collection refresh status changes
*/
private val refreshingStatusListeners = Collections.synchronizedList(LinkedList<WeakReference<RefreshingStatusListener>>())
@WorkerThread
override fun onHandleIntent(intent: Intent?) {
if (intent == null)
return
val id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1)
when (intent.action) {
ACTION_REFRESH_COLLECTIONS ->
if (runningRefresh.add(id)) {
refreshingStatusListeners.forEach { listener ->
listener.get()?.onDavRefreshStatusChanged(id, true)
}
refreshCollections(id)
}
ACTION_FORCE_SYNC -> {
val uri = intent.data!!
val authority = uri.authority!!
val account = Account(
uri.pathSegments[1],
uri.pathSegments[0]
)
forceSync(authority, account)
}
}
}
/* BOUND SERVICE PART
for communicating with the activities
*/
interface RefreshingStatusListener {
fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean)
}
private val binder = InfoBinder()
inner class InfoBinder: Binder() {
fun isRefreshing(id: Long) = runningRefresh.contains(id)
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediateIfRunning: Boolean) {
refreshingStatusListeners += WeakReference<RefreshingStatusListener>(listener)
if (callImmediateIfRunning)
synchronized(runningRefresh) {
for (id in runningRefresh)
listener.onDavRefreshStatusChanged(id, true)
}
}
fun removeRefreshingStatusListener(listener: RefreshingStatusListener) {
val iter = refreshingStatusListeners.iterator()
while (iter.hasNext()) {
val item = iter.next().get()
if (item == listener || item == null)
iter.remove()
}
}
}
override fun onBind(intent: Intent?) = binder
/* ACTION RUNNABLES
which actually do the work
*/
private fun forceSync(authority: String, account: Account) {
Logger.log.info("Forcing $authority synchronization of $account")
val extras = Bundle(2)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
ContentResolver.requestSync(account, authority, extras)
}
private fun refreshCollections(serviceId: Long) {
val syncAllCollections = settings.getBoolean(Settings.SYNC_ALL_COLLECTIONS)
val homeSetDao = db.homeSetDao()
val collectionDao = db.collectionDao()
val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found")
val account = Account(service.accountName, getString(R.string.account_type))
val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
/**
* Checks if the given URL defines home sets and adds them to the home set list.
*
* @param personal Whether this is the "outer" call of the recursion.
*
* *true* = found home sets belong to the current-user-principal; recurse if
* calendar proxies or group memberships are found
*
* *false* = found home sets don't directly belong to the current-user-principal; don't recurse
*
* @throws java.io.IOException
* @throws HttpException
* @throws at.bitfire.dav4jvm.exception.DavException
*/
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, personal: Boolean = true) {
val related = mutableSetOf<HttpUrl>()
fun findRelated(root: HttpUrl, dav: Response) {
// refresh home sets: calendar-proxy-read/write-for
dav[CalendarProxyReadFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyReadFor ->
related += proxyReadFor
}
}
}
dav[CalendarProxyWriteFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyWriteFor ->
related += proxyWriteFor
}
}
}
// refresh home sets: direct group memberships
dav[GroupMembership::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
root.resolve(href)?.let { groupMembership ->
related += groupMembership
}
}
}
}
val dav = DavResource(client, url)
when (service.type) {
Service.TYPE_CARDDAV ->
try {
dav.propfind(0, DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, personal, foundUrl)
}
}
if (personal)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for addressbook home sets", e)
else
throw e
}
Service.TYPE_CALDAV -> {
try {
dav.propfind(0, DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, personal, foundUrl)
}
}
if (personal)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for calendar home sets", e)
else
throw e
}
}
}
// query related homesets (those that do not belong to the current-user-principal)
for (resource in related)
queryHomeSets(client, resource, false)
}
fun saveHomesets() {
// syncAll sets the ID of the new homeset to the ID of the old one when the URLs are matching
DaoTools(homeSetDao).syncAll(
homeSetDao.getByService(serviceId),
homeSets,
{ it.url })
}
fun saveCollections() {
// syncAll sets the ID of the new collection to the ID of the old one when the URLs are matching
DaoTools(collectionDao).syncAll(
collectionDao.getByService(serviceId),
collections, { it.url }) { new, old ->
// use old settings of "force read only" and "sync", regardless of detection results
new.forceReadOnly = old.forceReadOnly
new.sync = old.sync
}
}
try {
Logger.log.info("Refreshing ${service.type} collections of service #$service")
// cancel previous notification
NotificationManagerCompat.from(this)
.cancel(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS)
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(this, AccountSettings(this, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
service.principal?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// now refresh homesets and their member collections
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val (homeSetUrl, homeSet) = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
try {
DavResource(httpClient, homeSetUrl).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
if (!response.isSuccess())
return@propfind
if (relation == Response.HrefRelation.SELF) {
// this response is about the homeset itself
homeSet.displayName = response[DisplayName::class.java]?.displayName
homeSet.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
}
// in any case, check whether the response is about a useable collection
val info = Collection.fromDavResponse(response) ?: return@propfind
info.serviceId = serviceId
info.refHomeSet = homeSet
info.confirmed = true
// whether new collections are selected for synchronization by default (controlled by managed setting)
info.sync = syncAllCollections
info.owner = response[Owner::class.java]?.href?.let { response.href.resolve(it) }
Logger.log.log(Level.FINE, "Found collection", info)
// remember usable collections
if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
collections[response.href] = info
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
}
}
// check/refresh unconfirmed collections
val collectionsIter = collections.entries.iterator()
while (collectionsIter.hasNext()) {
val currentCollection = collectionsIter.next()
val (url, info) = currentCollection
if (!info.confirmed)
try {
// this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed
info.homeSetId = null
DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val collection = Collection.fromDavResponse(response) ?: return@propfind
collection.serviceId = info.serviceId // use same service ID as previous entry
collection.confirmed = true
// remove unusable collections
if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
(collection.type == Collection.TYPE_WEBCAL && collection.source == null))
collectionsIter.remove()
else
// update this collection in list
currentCollection.setValue(collection)
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete collection only if it was not accessible (40x)
collectionsIter.remove()
else
throw e
}
}
}
db.runInTransaction {
saveHomesets()
// use refHomeSet (if available) to determine homeset ID
for (collection in collections.values)
collection.refHomeSet?.let { homeSet ->
collection.homeSetId = homeSet.id
}
saveCollections()
}
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Invalid account", e)
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
val debugIntent = DebugInfoActivity.IntentBuilder(this)
.withCause(e)
.withAccount(account)
.build()
val notify = NotificationUtils.newBuilder(this, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(getString(R.string.dav_service_refresh_failed))
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
NotificationManagerCompat.from(this)
.notify(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
} finally {
runningRefresh.remove(serviceId)
refreshingStatusListeners.mapNotNull { it.get() }.forEach {
it.onDavRefreshStatusChanged(serviceId, false)
}
}
}
}

View file

@ -17,9 +17,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.TaskProvider
import org.dmfs.tasks.contract.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.TaskListColumns
import org.dmfs.tasks.contract.TaskContract.*
import java.util.logging.Level
class LocalTaskList private constructor(

View file

@ -1,7 +1,11 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.setup
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.servicedetection
import android.content.Context
import at.bitfire.dav4jvm.DavResource
@ -15,6 +19,7 @@ import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.log.StringHandler
import at.bitfire.davdroid.ui.setup.LoginModel
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.builder.ReflectionToStringBuilder
@ -434,11 +439,11 @@ class DavResourceFinder(
// data classes
class Configuration(
val cardDAV: ServiceInfo?,
val calDAV: ServiceInfo?,
val cardDAV: ServiceInfo?,
val calDAV: ServiceInfo?,
val encountered401: Boolean,
val logs: String
val encountered401: Boolean,
val logs: String
) {
data class ServiceInfo(

View file

@ -0,0 +1,385 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.servicedetection
import android.accounts.Account
import android.app.PendingIntent
import android.content.Context
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorker
import androidx.lifecycle.Transformations
import androidx.work.*
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.*
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import com.google.common.util.concurrent.ListenableFuture
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.util.logging.Level
import javax.inject.Inject
import kotlin.collections.*
@HiltWorker
class RefreshCollectionsWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters
): Worker(appContext, workerParams) {
companion object {
const val ARG_SERVICE_ID = "serviceId"
const val REFRESH_COLLECTION_WORKER_TAG = "refreshCollectionWorker"
val DAV_COLLECTION_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
Owner.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
fun workerName(serviceId: Long): String {
return "$REFRESH_COLLECTION_WORKER_TAG-$serviceId"
}
/**
* Requests immediate refresh of a given service
*
* @param serviceId serviceId which is to be refreshed
*/
fun refreshCollections(context: Context, serviceId: Long) {
val arguments = Data.Builder()
.putLong(ARG_SERVICE_ID, serviceId)
.build()
val workRequest = OneTimeWorkRequestBuilder<RefreshCollectionsWorker>()
.setInputData(arguments)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
workerName(serviceId),
ExistingWorkPolicy.KEEP, // if refresh is already running, just continue
workRequest
)
}
/**
* Will tell whether a refresh worker with given service id and state exists
*
* @param serviceId the service which the worker(s) belong to
* @param workState state of worker to match
* @return boolean true if worker with matching state was found
*/
fun isWorkerInState(context: Context, serviceId: Long, workState: WorkInfo.State) = Transformations.map(
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(workerName(serviceId))
) { workInfoList -> workInfoList.any { workInfo -> workInfo.state == workState } }
}
@Inject lateinit var db: AppDatabase
@Inject lateinit var settings: SettingsManager
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> =
CallbackToFutureAdapter.getFuture { completer ->
val notification = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_STATUS)
.setSmallIcon(R.drawable.ic_foreground_notify)
.setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title))
.setContentText(applicationContext.getString(R.string.foreground_service_notify_text))
.setStyle(NotificationCompat.BigTextStyle())
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
completer.set(ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification))
}
override fun doWork(): Result {
val serviceId = inputData.getLong(ARG_SERVICE_ID, -1)
if (serviceId == -1L)
return Result.failure()
val syncAllCollections = settings.getBoolean(Settings.SYNC_ALL_COLLECTIONS)
val homeSetDao = db.homeSetDao()
val collectionDao = db.collectionDao()
val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found")
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
/**
* Checks if the given URL defines home sets and adds them to the home set list.
*
* @param personal Whether this is the "outer" call of the recursion.
*
* *true* = found home sets belong to the current-user-principal; recurse if
* calendar proxies or group memberships are found
*
* *false* = found home sets don't directly belong to the current-user-principal; don't recurse
*
* @throws java.io.IOException
* @throws HttpException
* @throws at.bitfire.dav4jvm.exception.DavException
*/
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, personal: Boolean = true) {
val related = mutableSetOf<HttpUrl>()
fun findRelated(root: HttpUrl, dav: Response) {
// refresh home sets: calendar-proxy-read/write-for
dav[CalendarProxyReadFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyReadFor ->
related += proxyReadFor
}
}
}
dav[CalendarProxyWriteFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyWriteFor ->
related += proxyWriteFor
}
}
}
// refresh home sets: direct group memberships
dav[GroupMembership::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
root.resolve(href)?.let { groupMembership ->
related += groupMembership
}
}
}
}
val dav = DavResource(client, url)
when (service.type) {
Service.TYPE_CARDDAV ->
try {
dav.propfind(0, DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, personal, foundUrl)
}
}
if (personal)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for addressbook home sets", e)
else
throw e
}
Service.TYPE_CALDAV -> {
try {
dav.propfind(0, DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, personal, foundUrl)
}
}
if (personal)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for calendar home sets", e)
else
throw e
}
}
}
// query related homesets (those that do not belong to the current-user-principal)
for (resource in related)
queryHomeSets(client, resource, false)
}
fun saveHomesets() {
// syncAll sets the ID of the new homeset to the ID of the old one when the URLs are matching
DaoTools(homeSetDao).syncAll(
homeSetDao.getByService(serviceId),
homeSets,
{ it.url })
}
fun saveCollections() {
// syncAll sets the ID of the new collection to the ID of the old one when the URLs are matching
DaoTools(collectionDao).syncAll(
collectionDao.getByService(serviceId),
collections, { it.url }) { new, old ->
// use old settings of "force read only" and "sync", regardless of detection results
new.forceReadOnly = old.forceReadOnly
new.sync = old.sync
}
}
try {
Logger.log.info("Refreshing ${service.type} collections of service #$service")
// cancel previous notification
NotificationManagerCompat.from(applicationContext)
.cancel(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS)
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(applicationContext, AccountSettings(applicationContext, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
service.principal?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// now refresh homesets and their member collections
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val (homeSetUrl, homeSet) = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
try {
DavResource(httpClient, homeSetUrl).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
if (!response.isSuccess())
return@propfind
if (relation == Response.HrefRelation.SELF) {
// this response is about the homeset itself
homeSet.displayName = response[DisplayName::class.java]?.displayName
homeSet.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
}
// in any case, check whether the response is about a useable collection
val info = Collection.fromDavResponse(response) ?: return@propfind
info.serviceId = serviceId
info.refHomeSet = homeSet
info.confirmed = true
// whether new collections are selected for synchronization by default (controlled by managed setting)
info.sync = syncAllCollections
info.owner = response[Owner::class.java]?.href?.let { response.href.resolve(it) }
Logger.log.log(Level.FINE, "Found collection", info)
// remember usable collections
if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
collections[response.href] = info
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
}
}
// check/refresh unconfirmed collections
val collectionsIter = collections.entries.iterator()
while (collectionsIter.hasNext()) {
val currentCollection = collectionsIter.next()
val (url, info) = currentCollection
if (!info.confirmed)
try {
// this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed
info.homeSetId = null
DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val collection = Collection.fromDavResponse(response) ?: return@propfind
collection.serviceId = info.serviceId // use same service ID as previous entry
collection.confirmed = true
// remove unusable collections
if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
(collection.type == Collection.TYPE_WEBCAL && collection.source == null))
collectionsIter.remove()
else
// update this collection in list
currentCollection.setValue(collection)
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete collection only if it was not accessible (40x)
collectionsIter.remove()
else
throw e
}
}
}
db.runInTransaction {
saveHomesets()
// use refHomeSet (if available) to determine homeset ID
for (collection in collections.values)
collection.refHomeSet?.let { homeSet ->
collection.homeSetId = homeSet.id
}
saveCollections()
}
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Invalid account", e)
return Result.failure()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
val debugIntent = DebugInfoActivity.IntentBuilder(applicationContext)
.withCause(e)
.withAccount(account)
.build()
val notify = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(applicationContext.getString(R.string.refresh_collections_worker_refresh_failed))
.setContentText(applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(applicationContext, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
NotificationManagerCompat.from(applicationContext)
.notify(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
return Result.failure()
}
// Success
return Result.success()
}
}

View file

@ -22,9 +22,9 @@ import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.util.DateUtils
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.component.VAlarm
import okhttp3.HttpUrl

View file

@ -775,7 +775,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
.setPriority(priority)
.setCategory(NotificationCompat.CATEGORY_ERROR)
viewItemAction?.let { builder.addAction(it) }
builder.addAction(buildRetryAction())
notificationManager.notify(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build())
}
@ -796,32 +795,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
.withRemoteResource(remote)
.build()
private fun buildRetryAction(): NotificationCompat.Action {
val retryIntent = Intent(context, DavService::class.java)
retryIntent.action = DavService.ACTION_FORCE_SYNC
val syncAuthority: String
val syncAccount: Account
if (authority == ContactsContract.AUTHORITY) {
// if this is a contacts sync, retry syncing all address books of the main account
syncAuthority = context.getString(R.string.address_books_authority)
syncAccount = mainAccount
} else {
syncAuthority = authority
syncAccount = account
}
retryIntent.data = Uri.parse("sync://").buildUpon()
.authority(syncAuthority)
.appendPath(syncAccount.type)
.appendPath(syncAccount.name)
.build()
return NotificationCompat.Action(
android.R.drawable.ic_menu_rotate, context.getString(R.string.sync_error_retry),
PendingIntent.getService(context, 0, retryIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
}
private fun buildViewItemAction(local: ResourceType): NotificationCompat.Action? {
Logger.log.log(Level.FINE, "Adding view action for local resource", local)
val intent = local.id?.let { id ->

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.provider.Settings
import android.view.*
import androidx.core.content.ContextCompat
import androidx.core.content.PackageManagerCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@ -33,7 +32,6 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.DavUtils.SyncStatus
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.StorageLowReceiver
import at.bitfire.davdroid.databinding.AccountListBinding

View file

@ -43,8 +43,8 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat
import at.bitfire.ical4android.TaskProvider.ProviderName
import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat
import at.techbee.jtx.JtxContract
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel

View file

@ -28,6 +28,7 @@ object NotificationUtils {
const val NOTIFY_INVALID_RESOURCE = 11
const val NOTIFY_WEBDAV_ACCESS = 12
const val NOTIFY_LOW_STORAGE = 13
const val NOTIFY_SYNC_EXPEDITED = 14
const val NOTIFY_TASKS_PROVIDER_TOO_OLD = 20
const val NOTIFY_PERMISSIONS = 21

View file

@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui.account
import android.content.*
import android.os.Bundle
import android.os.IBinder
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.view.*
@ -23,14 +22,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewbinding.ViewBinding
import androidx.work.WorkInfo
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AccountCollectionsBinding
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.ui.PermissionsActivity
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -278,7 +278,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
@Assisted val accountModel: AccountActivity.Model,
@Assisted val serviceId: Long,
@Assisted val collectionType: String
): ViewModel(), DavService.RefreshingStatusListener, SyncStatusObserver {
): ViewModel(), SyncStatusObserver {
@AssistedFactory
interface Factory {
@ -302,21 +302,8 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
}
}
// observe DavService refresh status
@Volatile
private var davService: DavService.InfoBinder? = null
private var davServiceConn: ServiceConnection? = null
private val svcConn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val svc = service as DavService.InfoBinder
davService = svc
svc.addRefreshingStatusListener(this@Model, true)
}
override fun onServiceDisconnected(name: ComponentName?) {
davService = null
}
}
val isRefreshing = MutableLiveData<Boolean>()
// observe RefreshCollectionsWorker status
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(context, serviceId, WorkInfo.State.RUNNING)
// observe whether sync is active
private var syncStatusHandle: Any? = null
@ -325,9 +312,6 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
init {
if (context.bindService(Intent(context, DavService::class.java), svcConn, Context.BIND_AUTO_CREATE))
davServiceConn = svcConn
viewModelScope.launch(Dispatchers.Default) {
syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model)
checkSyncStatus()
@ -336,22 +320,10 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
override fun onCleared() {
syncStatusHandle?.let { ContentResolver.removeStatusChangeListener(it) }
davService?.removeRefreshingStatusListener(this)
davServiceConn?.let {
context.unbindService(it)
davServiceConn = null
}
}
fun refresh() {
DavService.refreshCollections(context, serviceId)
}
@AnyThread
override fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean) {
if (id == serviceId)
isRefreshing.postValue(refreshing)
RefreshCollectionsWorker.refreshCollections(context, serviceId)
}
@AnyThread

View file

@ -15,13 +15,13 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.*
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.ExceptionInfoFragment
import dagger.assisted.Assisted
@ -148,7 +148,7 @@ class CreateCollectionFragment: DialogFragment() {
db.collectionDao().insert(collection)
// trigger service detection (because the collection may have other properties than the ones we have inserted)
DavService.refreshCollections(context, service.id)
RefreshCollectionsWorker.refreshCollections(context, service.id)
}
// post success

View file

@ -15,11 +15,6 @@ import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS
import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMISSIONS
import at.bitfire.davdroid.R
import at.bitfire.ical4android.TaskProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoSet
import javax.inject.Inject
class PermissionsIntroFragment : Fragment() {

View file

@ -17,11 +17,6 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.TasksFragment
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import javax.inject.Inject
class TasksIntroFragment : Fragment() {

View file

@ -20,7 +20,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.*
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.LoginAccountDetailsBinding
@ -30,6 +29,8 @@ import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
@ -174,9 +175,6 @@ class AccountDetailsFragment : Fragment() {
val accountSettings = AccountSettings(context, account)
val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL)
val refreshIntent = Intent(context, DavService::class.java)
refreshIntent.action = DavService.ACTION_REFRESH_COLLECTIONS
val addrBookAuthority = context.getString(R.string.address_books_authority)
if (config.cardDAV != null) {
// insert CardDAV service
@ -186,8 +184,7 @@ class AccountDetailsFragment : Fragment() {
accountSettings.setGroupMethod(groupMethod)
// start CardDAV service detection (refresh collections)
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
context.startService(refreshIntent)
RefreshCollectionsWorker.refreshCollections(context, id)
// set default sync interval and enable sync regardless of permissions
ContentResolver.setIsSyncable(account, addrBookAuthority, 1)
@ -200,8 +197,7 @@ class AccountDetailsFragment : Fragment() {
val id = insertService(name, Service.TYPE_CALDAV, config.calDAV)
// start CalDAV service detection (refresh collections)
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
context.startService(refreshIntent)
RefreshCollectionsWorker.refreshCollections(context, id)
// set default sync interval and enable sync regardless of permissions
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)

View file

@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.ui.DebugInfoActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference

View file

@ -6,6 +6,7 @@ package at.bitfire.davdroid.ui.setup
import androidx.lifecycle.ViewModel
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import java.net.URI
class LoginModel: ViewModel() {

View file

@ -26,7 +26,6 @@ import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import androidx.annotation.WorkerThread
import androidx.core.content.getSystemService
import androidx.lifecycle.Observer
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
@ -38,7 +37,6 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.DaoTools
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import at.bitfire.davdroid.webdav.cache.HeadResponseCache

View file

@ -43,9 +43,9 @@
<string name="account_list_empty">مرحباً بك في DAVx⁵ !\n\n يمكنك إضافة حساب CalDAV أو CardDAV الآن.</string>
<string name="accounts_global_sync_disabled">تم تعطيل المزامنة التلقائية على مستوى النظام</string>
<string name="accounts_global_sync_enable">تفعيل</string>
<!--DavService-->
<string name="dav_service_refresh_failed">فشل اكتشاف الخدمة</string>
<string name="dav_service_refresh_couldnt_refresh">لم يتمكن التطبيق من تجديد قائمة المجموعة</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">فشل اكتشاف الخدمة</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">لم يتمكن التطبيق من تجديد قائمة المجموعة</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Автоматичното синхронизиране е изключено на ниво система</string>
<string name="accounts_global_sync_enable">Включване</string>
<string name="accounts_sync_all">Синхронизиране на всички профили</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Откриването на услугите се провали</string>
<string name="dav_service_refresh_couldnt_refresh">Грешка при презареждане</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Откриването на услугите се провали</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Грешка при презареждане</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Не може да работи на преден план</string>
<string name="battery_optimization_notify_text">Необходимо е да спрете оптимизирането на батерията за приложението</string>

View file

@ -133,9 +133,9 @@
<string name="accounts_global_sync_disabled">La sincronització automàtica de tot el sistema està inhabilitada</string>
<string name="accounts_global_sync_enable">Activa</string>
<string name="accounts_sync_all">Sincronitza tots els comptes</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Ha fallat la detecció del servei</string>
<string name="dav_service_refresh_couldnt_refresh">No s\'ha pogut actualitzar la llista de col·leccions</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Ha fallat la detecció del servei</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">No s\'ha pogut actualitzar la llista de col·leccions</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">No s\'ha pogut executar en segon pla</string>
<string name="battery_optimization_notify_text">Es requereix incloure-la a la llista blanca d\'optimització de la bateria</string>

View file

@ -130,9 +130,9 @@
<string name="accounts_global_sync_disabled">Automatická synchronizace v rámci celého systému je vypnutá</string>
<string name="accounts_global_sync_enable">Zapnout</string>
<string name="accounts_sync_all">Synchronizovat všechny účty</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Vyhledání služby se nezdařilo</string>
<string name="dav_service_refresh_couldnt_refresh">Nedaří se znovu načíst seznam sady</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Vyhledání služby se nezdařilo</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Nedaří se znovu načíst seznam sady</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Běh na pozadí není zařízením umožňován</string>
<string name="battery_optimization_notify_text">Je zapotřebí zařadit na seznam výjimek z optimalizace akumulátoru</string>

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Automatisk synkronisering på tværs af systemet er deaktiveret</string>
<string name="accounts_global_sync_enable">Aktivere</string>
<string name="accounts_sync_all">Synkronisere alle konti</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Registrering af tjeneste kunne ikke foretages</string>
<string name="dav_service_refresh_couldnt_refresh">Kunne ikke opdatere samling liste</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Registrering af tjeneste kunne ikke foretages</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Kunne ikke opdatere samling liste</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Kan ikke køre i forgrunden</string>
<string name="battery_optimization_notify_text">Batteri optimering hvid listet påkrævet</string>

View file

@ -135,9 +135,9 @@
<string name="accounts_global_sync_disabled">Systemweite automatische Synchronisierung ist nicht aktiv</string>
<string name="accounts_global_sync_enable">Aktivieren</string>
<string name="accounts_sync_all">Alle Konten synchronisieren</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Diensterkennung fehlgeschlagen</string>
<string name="dav_service_refresh_couldnt_refresh">Ordnerliste konnte nicht aktualisiert werden</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Diensterkennung fehlgeschlagen</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Ordnerliste konnte nicht aktualisiert werden</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Darf nicht im Vordergrund ausgeführt werden</string>
<string name="battery_optimization_notify_text">Ausnahme von Akku-Optimierung erforderlich</string>

View file

@ -112,9 +112,9 @@
<string name="accounts_global_sync_disabled">Ο αυτόματος συγχρονισμός σε όλο το σύστημα είναι απενεργοποιημένος</string>
<string name="accounts_global_sync_enable">Ενεργοποίηση</string>
<string name="accounts_sync_all">Συγχρονισμός όλων των λογαριασμών</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Αποτυχία ανίχνευσης υπηρεσίας</string>
<string name="dav_service_refresh_couldnt_refresh">Αδυναμία ανανέωσης της λίστας συλλογής</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Αποτυχία ανίχνευσης υπηρεσίας</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Αδυναμία ανανέωσης της λίστας συλλογής</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Αδυναμία εκτέλεσης στο προσκήνιο</string>
<!--ForegroundService-->

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Sincronización automática del sistema completo está deshabilitada</string>
<string name="accounts_global_sync_enable">Activar</string>
<string name="accounts_sync_all">Sincronizar todas las cuentas</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Falló la detección del servicio</string>
<string name="dav_service_refresh_couldnt_refresh">No se pudo refrescar lista de colección</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Falló la detección del servicio</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">No se pudo refrescar lista de colección</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">No puede ejecutarse en primer plano</string>
<string name="battery_optimization_notify_text">Se requiere agregar a la lista blanca de optimización de batería</string>

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Sistema osoko sinkronizazio automatikoa desgaituta dago</string>
<string name="accounts_global_sync_enable">Gaitu</string>
<string name="accounts_sync_all">Sinkronizatu kontu guztiak</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Zerbitzuaren detekzioak huts egin du</string>
<string name="dav_service_refresh_couldnt_refresh">Ezin izan da bilduma zerrenda freskatu</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zerbitzuaren detekzioak huts egin du</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Ezin izan da bilduma zerrenda freskatu</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Ezin da aurrealdean exekutatu</string>
<string name="battery_optimization_notify_text">Bateriaren optimizazioaren zerrenda zuria behar da</string>

View file

@ -106,9 +106,9 @@
<string name="accounts_global_sync_disabled">همگام سازی خودکار در کل سیستم غیرفعال است</string>
<string name="accounts_global_sync_enable">فعال</string>
<string name="accounts_sync_all">همگام سازی همه حساب‌ها</string>
<!--DavService-->
<string name="dav_service_refresh_failed">تشخیص سرویس ناموفق بود</string>
<string name="dav_service_refresh_couldnt_refresh">لیست مجموعه به روز نشد</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">تشخیص سرویس ناموفق بود</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">لیست مجموعه به روز نشد</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<string name="foreground_service_notify_title">در حال اجرا در پیش زمینه</string>

View file

@ -122,9 +122,9 @@
<string name="accounts_global_sync_disabled">La synchronisation automatique globale est désactivée</string>
<string name="accounts_global_sync_enable">Activer</string>
<string name="accounts_sync_all">Synchroniser tous les comptes</string>
<!--DavService-->
<string name="dav_service_refresh_failed">La détection du service a échoué</string>
<string name="dav_service_refresh_couldnt_refresh">Impossible d\'actualiser la liste de collection</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">La détection du service a échoué</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Impossible d\'actualiser la liste de collection</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<string name="foreground_service_notify_title">Fonctionne au premier plan</string>

View file

@ -135,9 +135,9 @@
<string name="accounts_global_sync_disabled">A sincronización global automática do sistema está desactivada</string>
<string name="accounts_global_sync_enable">Activar</string>
<string name="accounts_sync_all">Sincroniza todas as contas</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Fallou a detección do servizo</string>
<string name="dav_service_refresh_couldnt_refresh">Non se actualizou a lista da colección</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Fallou a detección do servizo</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Non se actualizou a lista da colección</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Non pode executarse en segundo plano</string>
<string name="battery_optimization_notify_text">Require ter permiso para optimizacións de batería</string>

View file

@ -110,9 +110,9 @@
<string name="accounts_global_sync_disabled">Automatska sinkronizacija je onemogućena na razini sustava</string>
<string name="accounts_global_sync_enable">Omogući</string>
<string name="accounts_sync_all">Sinkroniziraj sve račune</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Detekcija servisa nije uspjela</string>
<string name="dav_service_refresh_couldnt_refresh">Nije moguće osvježiti popis zbirki</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Detekcija servisa nije uspjela</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Nije moguće osvježiti popis zbirki</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<string name="foreground_service_notify_title">Pokrenuto u prednjem planu</string>

View file

@ -125,9 +125,9 @@
<string name="accounts_global_sync_disabled">A rendszerszintű automatikus szinkronizálás ki van kapcsolva</string>
<string name="accounts_global_sync_enable">Bekapcsolás</string>
<string name="accounts_sync_all">Az összes fiók szinkronizálása</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Szolgáltatások felderítése nem sikerült</string>
<string name="dav_service_refresh_couldnt_refresh">Gyűjteménylista frissítése nem sikerült</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Szolgáltatások felderítése nem sikerült</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Gyűjteménylista frissítése nem sikerült</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Nem lehetséges az előtérben való futtatás</string>
<string name="battery_optimization_notify_text">Kivétel az akkumulátorhasználat optimalizálása alól</string>

View file

@ -116,9 +116,9 @@
<string name="accounts_global_sync_disabled">La sincronizzazione automatica dell\'intero sistema è disabilitata</string>
<string name="accounts_global_sync_enable">Attiva</string>
<string name="accounts_sync_all">Sincronizzazione di tutti gli account</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Fallita l\'individuazione dei servizi</string>
<string name="dav_service_refresh_couldnt_refresh">Impossibile aggiornare la lista delle raccolte</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Fallita l\'individuazione dei servizi</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Impossibile aggiornare la lista delle raccolte</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<string name="foreground_service_notify_title">Esecuzione in primo piano</string>

View file

@ -133,9 +133,9 @@
<string name="accounts_global_sync_disabled">システム全体の自動同期が無効です</string>
<string name="accounts_global_sync_enable">有効</string>
<string name="accounts_sync_all">すべてのアカウントを同期</string>
<!--DavService-->
<string name="dav_service_refresh_failed">サービスの検出に失敗しました</string>
<string name="dav_service_refresh_couldnt_refresh">コレクション リストを更新できませんでした</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">サービスの検出に失敗しました</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">コレクション リストを更新できませんでした</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">フォアグラウンドで実行できません</string>
<string name="battery_optimization_notify_text">バッテリー最適化を無効にしてください</string>

View file

@ -126,9 +126,9 @@
<string name="accounts_global_sync_disabled">시스템 전체 자동 동기화가 비활성화됩니다.</string>
<string name="accounts_global_sync_enable">활성</string>
<string name="accounts_sync_all">모든 계정 동기화</string>
<!--DavService-->
<string name="dav_service_refresh_failed">서비스 검색 실패</string>
<string name="dav_service_refresh_couldnt_refresh">collection 목록을 새로 고칠 수 없습니다.</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">서비스 검색 실패</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">collection 목록을 새로 고칠 수 없습니다.</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">foreground에서 실행할 수 없습니다.</string>
<string name="battery_optimization_notify_text">배터리 최적화 허용목록 필요</string>

View file

@ -44,9 +44,9 @@
<string name="account_list_empty">Velkommen til DAVx⁵.\n\nDu kan legge til en CalDAV/CardDAV-konto nå.</string>
<string name="accounts_global_sync_disabled">Systemomspennende automatisk synkronisering avskrudd</string>
<string name="accounts_global_sync_enable">Skru på</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Tjenesteoppdagelse mislyktes</string>
<string name="dav_service_refresh_couldnt_refresh">Kunne ikke gjenoppfriske innsamlingsliste</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Tjenesteoppdagelse mislyktes</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Kunne ikke gjenoppfriske innsamlingsliste</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Automatisch synchroniseren is voor alle accounts uitgeschakeld</string>
<string name="accounts_global_sync_enable">Inschakelen</string>
<string name="accounts_sync_all">Alle accounts synchroniseren</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Service herkenning is mislukt</string>
<string name="dav_service_refresh_couldnt_refresh">De collectielijst is niet bijgewerkt</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Service herkenning is mislukt</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">De collectielijst is niet bijgewerkt</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Kan niet op de voorgrond draaien</string>
<string name="battery_optimization_notify_text">Toestemming tot onbeperkt batterijgebruik is vereist</string>

View file

@ -135,9 +135,9 @@
<string name="accounts_global_sync_disabled">Automatyczna synchronizacja dla całego systemu jest wyłączona</string>
<string name="accounts_global_sync_enable">Włącz</string>
<string name="accounts_sync_all">Synchronizuj wszystkie konta</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Wykrycie serwisu nie powiodło się</string>
<string name="dav_service_refresh_couldnt_refresh">Nie można odświeżyć listy kolekcji</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Wykrycie serwisu nie powiodło się</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Nie można odświeżyć listy kolekcji</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Nie można uruchomić na pierwszym planie</string>
<string name="battery_optimization_notify_text">Wymagana biała lista optymalizacji baterii</string>

View file

@ -100,9 +100,9 @@
<string name="accounts_global_sync_disabled">A sincronização automática pelo sistema está desativada</string>
<string name="accounts_global_sync_enable">Ativar</string>
<string name="accounts_sync_all">Sincronizar todas as contas</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Falha na detecção do serviço</string>
<string name="dav_service_refresh_couldnt_refresh">Não foi possível atualizar a lista da coleção</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Falha na detecção do serviço</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Não foi possível atualizar a lista da coleção</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -135,9 +135,9 @@
<string name="accounts_global_sync_disabled">Синхронизация отключена на уровне устройства</string>
<string name="accounts_global_sync_enable">Включить</string>
<string name="accounts_sync_all">Синхронизировать все аккаунты</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Не удалось обнаружить службу</string>
<string name="dav_service_refresh_couldnt_refresh">Не удалось обновить список коллекций</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Не удалось обнаружить службу</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Не удалось обновить список коллекций</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Невозможно запустить в приоритетном режиме</string>
<string name="battery_optimization_notify_text">Необходимо добавить в белый список оптимизации батареи</string>

View file

@ -47,9 +47,9 @@
<string name="account_list_empty">Vitajte v programe DAVx⁵!\n\nTeraz môžete pridať používateľský účet pre CalDAV/CardDAV .</string>
<string name="accounts_global_sync_disabled">Automatická synchronizácia platná pre celý systém je zakázaná</string>
<string name="accounts_global_sync_enable">Povoliť</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Zisťovanie služby zlyhalo</string>
<string name="dav_service_refresh_couldnt_refresh">Nie je možné obnoviť zoznam kolekcií</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zisťovanie služby zlyhalo</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Nie je možné obnoviť zoznam kolekcií</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -43,9 +43,9 @@
<string name="account_list_empty">Dobrodošli v DAVx⁵!\n\nZdaj lahko dodate CalDAV/CardDAV račun.</string>
<string name="accounts_global_sync_disabled">Sistemska avtomatska sinhronizacija je izklopljena</string>
<string name="accounts_global_sync_enable">Omogoči</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Zaznava storitve ni uspela</string>
<string name="dav_service_refresh_couldnt_refresh">Zbirke ni bilo mogoče osvežiti</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Zaznava storitve ni uspela</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Zbirke ni bilo mogoče osvežiti</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -83,9 +83,9 @@
<string name="accounts_global_sync_disabled">Синхронизација је системски искључена</string>
<string name="accounts_global_sync_enable">Укључи</string>
<string name="accounts_sync_all">Синхронизуј све налоге</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Откривање услуге није успело</string>
<string name="dav_service_refresh_couldnt_refresh">Не могох да освежим списак збирки</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Откривање услуге није успело</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Не могох да освежим списак збирки</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -133,9 +133,9 @@
<string name="accounts_global_sync_disabled">Systemomfattande automatisk synkronisering är inaktiverad</string>
<string name="accounts_global_sync_enable">Aktivera</string>
<string name="accounts_sync_all">Synkronisera alla konton</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Servicedetektering misslyckades</string>
<string name="dav_service_refresh_couldnt_refresh">Det gick inte att uppdatera samlingslistan</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Servicedetektering misslyckades</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Det gick inte att uppdatera samlingslistan</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Kan inte köras i förgrunden</string>
<string name="battery_optimization_notify_text">Vitlista för batterioptimering krävs</string>

View file

@ -66,9 +66,9 @@
<string name="account_list_empty">Witōmy w DAVx⁵!\n\nMożesz teroz przidać kōnto CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">Autōmatyczno synchrōnizacyjo dlo cołkigo systymu je zastawiōno</string>
<string name="accounts_global_sync_enable">Włōncz</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Niy szło ôdświyżyć serwisu</string>
<string name="dav_service_refresh_couldnt_refresh">Niy szło ôdświyżyć listy kolekcyje</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Niy szło ôdświyżyć serwisu</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Niy szło ôdświyżyć listy kolekcyje</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -29,9 +29,9 @@
<string name="navigation_drawer_faq">SSS</string>
<string name="navigation_drawer_donate">Bağış yap</string>
<string name="account_list_empty">DAVx⁵\'e hoşgeldin!\n\nŞimdi bir CalDAV/CardDAV hesabı ekleyebilirsin.</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Servis keşfi başarısız</string>
<string name="dav_service_refresh_couldnt_refresh">Kolleksiyon listesi yenilenemedi</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Servis keşfi başarısız</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Kolleksiyon listesi yenilenemedi</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -80,9 +80,9 @@
<string name="accounts_global_sync_disabled">Автоматичну синхронізацію вимкнено зі сторони системи</string>
<string name="accounts_global_sync_enable">Увімкнути</string>
<string name="accounts_sync_all">Синхронізувати всі обліківки</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Не вдалося виявити сервіси</string>
<string name="dav_service_refresh_couldnt_refresh">Не вдалося оновити перелік колекції</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Не вдалося виявити сервіси</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Не вдалося оновити перелік колекції</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -127,9 +127,9 @@
<string name="accounts_global_sync_disabled">Đồng bộ hoá tự động trên toàn hệ thống bị tắt</string>
<string name="accounts_global_sync_enable">Bật</string>
<string name="accounts_sync_all">Đồng bộ tất cả tài khoản</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Dò tìm dịch vụ thất bại</string>
<string name="dav_service_refresh_couldnt_refresh">Không thể làm mới danh sách bộ sưu tập</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Dò tìm dịch vụ thất bại</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Không thể làm mới danh sách bộ sưu tập</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">Không thể chạy ở trước</string>
<string name="battery_optimization_notify_text">Bắt buộc không tối ưu hoá pin</string>

View file

@ -72,9 +72,9 @@
<string name="account_list_empty">歡迎使用 DAVx⁵!\n\n您現在可以新增 CalDAV/CardDAV 帳號</string>
<string name="accounts_global_sync_disabled">操作系統的自動同步被關閉了</string>
<string name="accounts_global_sync_enable">啟用</string>
<!--DavService-->
<string name="dav_service_refresh_failed">未發現遠端服務</string>
<string name="dav_service_refresh_couldnt_refresh">無法更新清單</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">未發現遠端服務</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">無法更新清單</string>
<!--Battery Optimization-->
<!--ForegroundService-->
<!--AppSettingsActivity-->

View file

@ -135,9 +135,9 @@
<string name="accounts_global_sync_disabled">系统全局自动同步已禁用</string>
<string name="accounts_global_sync_enable">启用</string>
<string name="accounts_sync_all">同步所有账户</string>
<!--DavService-->
<string name="dav_service_refresh_failed">服务配置检测失败</string>
<string name="dav_service_refresh_couldnt_refresh">无法刷新集合列表</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">服务配置检测失败</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">无法刷新集合列表</string>
<!--Battery Optimization-->
<string name="battery_optimization_notify_title">无法在前台运行</string>
<string name="battery_optimization_notify_text">需要在电池优化中排除本应用</string>

View file

@ -150,9 +150,9 @@
<string name="accounts_global_sync_enable">Enable</string>
<string name="accounts_sync_all">Sync all accounts</string>
<!-- DavService -->
<string name="dav_service_refresh_failed">Service detection failed</string>
<string name="dav_service_refresh_couldnt_refresh">Couldn\'t refresh collection list</string>
<!-- RefreshCollectionsWorker -->
<string name="refresh_collections_worker_refresh_failed">Service detection failed</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Couldn\'t refresh collection list</string>
<!-- Battery Optimization -->
<string name="battery_optimization_notify_title">Can not run in foreground</string>