Compare commits

...

15 Commits

Author SHA1 Message Date
Ricki Hirner
0854c36792 Version bump to 3.1-beta3 2020-05-05 15:06:36 +02:00
Ricki Hirner
4f9cdaff15 Fetch translations from Transifex 2020-05-05 15:06:03 +02:00
Ricki Hirner
afaeec4810 Handle exceptions when event/task SEQUENCE is increased; update ical4android 2020-05-05 14:48:25 +02:00
Ricki Hirner
427a24ccf6 Show Donate in Navigation drawer only in ose 2020-05-05 11:40:49 +02:00
Ricki Hirner
a970872790 Version bump to 3.1-beta2 2020-05-04 23:15:03 +02:00
Ricki Hirner
86667b426d Make sure Thread.getContextClassLoader is set while syncing (for ical4j)
* use dav4jvm version that doesn't depend on ServiceLoader anymore
2020-05-04 23:14:31 +02:00
Ricki Hirner
e003402fa2 Use coroutines instead of threads, when possible 2020-05-04 23:13:15 +02:00
Ricki Hirner
729f9e952b Permissions activity: add App settings button 2020-05-03 13:48:22 +02:00
Ricki Hirner
e8a7221f44 Sync algorithm: use Kotlin coroutines instead of thread-pool executors 2020-05-02 15:57:13 +02:00
Ricki Hirner
8f90ad156c Update dependencies; version bump to 3.1.0-beta1 2020-05-02 12:35:41 +02:00
Ricki Hirner
938982bf82 New permissions model 2020-05-02 12:35:26 +02:00
Ricki Hirner
35e2c52de2 Update gradle plugin 2020-05-02 11:57:42 +02:00
Ricki Hirner
b7377f33c2 Sync cancellation: show in logs, cancel whole thread group 2020-05-02 11:57:40 +02:00
Ricki Hirner
321671c629 Version bump to 3.0.1-beta1 2020-05-02 11:56:36 +02:00
Ricki Hirner
8df07108d7 Use ical4j 3.x 2020-05-02 11:56:12 +02:00
89 changed files with 1759 additions and 586 deletions

View File

@@ -20,7 +20,8 @@ android {
defaultConfig {
applicationId "at.bitfire.davdroid"
versionCode 300000004
versionCode 301000002
versionName '3.1-beta3'
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
buildConfigField "boolean", "customCerts", "true"
@@ -32,6 +33,9 @@ android {
}
compileOptions {
// enable because ical4android requires desugaring
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@@ -39,12 +43,12 @@ android {
jvmTarget = "1.8"
}
dataBinding.enabled = true
buildFeatures.dataBinding = true
flavorDimensions "distribution"
productFlavors {
standard {
versionName "3.0-ose2"
versionName "3.1-beta3-ose"
}
}
@@ -106,7 +110,9 @@ dependencies {
implementation project(':ical4android')
implementation project(':vcard4android')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
implementation 'androidx.cardview:cardview:1.0.0'

View File

@@ -1,35 +1,20 @@
# ProGuard usage for DAVx⁵:
# shrinking yes (main reason for using ProGuard)
# ProGuard/R8 usage for DAVx⁵:
# shrinking yes
# optimization yes
# obfuscation no (DAVx⁵ is open-source)
# preverification no
# obfuscation no (open-source)
-dontobfuscate
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# ez-vcard
-dontwarn ezvcard.io.json.** # JSON serializer (for jCards) not used
-dontwarn freemarker.** # freemarker templating library (for creating hCards) not used
-dontwarn org.jsoup.** # jsoup library (for hCard parsing) not used
-keep class ezvcard.property.** { *; } # keep all vCard properties (created at runtime)
# ical4j: ignore unused dynamic libraries
-dontwarn aQute.**
-dontwarn groovy.** # Groovy-based ContentBuilder not used
-dontwarn javax.cache.** # no JCache support in Android
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
# ical4j
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class org.threeten.bp.** { *; } # keep ThreeTen (for time zone processing)
# dnsjava
-dontwarn sun.net.spi.nameservice.** # not available on Android
# DAVx⁵ + libs
-keep class at.bitfire.** { *; } # all DAVx⁵ code is required

View File

@@ -71,6 +71,7 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -100,6 +101,7 @@
android:label="@string/debug_info_title">
<intent-filter>
<action android:name="android.intent.action.BUG_REPORT"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
@@ -112,6 +114,8 @@
android:resource="@xml/debug_paths" />
</provider>
<activity android:name=".ui.PermissionsActivity" />
<!-- account type "DAVx⁵" -->
<service
android:name=".syncadapter.AccountAuthenticatorService"

View File

@@ -1 +1 @@
{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","zagur"],"cs":["pavelb","tomas.odehnal"],"da":["knutztar","mjjzf","Tntdruid_","twikedk"],"de":["anestiskaci","Atalanttore","corppneq","maxkl","nicolas_git","owncube","TheName","Wyrrrd","YvanM"],"el":["anestiskaci","diamond_gr"],"es":["aluaces","Ark74","Elhea","GranPC","jcvielma","plaguna","polkhas","xphnx"],"eu":["cockeredradiation","Osoitz"],"fa":["ahangarha","amiraliakbari","maryambehzi","mtashackori","Numb","taranehsaei"],"fi_FI":["raketti","tseipii"],"fr":["AlainR","alkino2","Amadeen","boutil","callmemagnus","chfo","chrcha","Floflr","grenatrad","Jorg722","Llorc","Novick","Poussinou","Thecross","YvanM","ÉricB."],"fr_FR":["chrcha","Llorc","Poussinou"],"gl":["aluaces","pikamoku"],"hu":["jtg"],"it":["Damtux","ed0","FranzMari","noccio","nwandy","rickyroo","technezio"],"ja":["Naofumi"],"nb_NO":["elonus"],"nl":["davtemp","dehart","toonvangerwen","XtremeNova"],"pl":["gsz","mg6","oskarjakiela","TheName","TORminator"],"pt_BR":["amalvarenga","wanderlei.huttel"],"ru":["aigoshin","anm","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy"]}
{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","zagur"],"cs":["pavelb","tomas.odehnal"],"da":["knutztar","mjjzf","Tntdruid_","twikedk"],"de":["anestiskaci","Atalanttore","corppneq","maxkl","nicolas_git","owncube","TheName","Wyrrrd","YvanM"],"el":["anestiskaci","diamond_gr","KristinaQejvanaj"],"es":["aluaces","Ark74","Elhea","GranPC","jcvielma","plaguna","polkhas","xphnx"],"eu":["cockeredradiation","Osoitz","Thadah"],"fa":["ahangarha","amiraliakbari","joojoojoo","maryambehzi","mtashackori","Numb","taranehsaei"],"fi_FI":["raketti","tseipii"],"fr":["AlainR","alkino2","Amadeen","boutil","callmemagnus","chfo","chrcha","Floflr","grenatrad","jokx","Jorg722","Llorc","Novick","Poussinou","Thecross","YvanM","ÉricB."],"fr_FR":["chrcha","Llorc","Poussinou"],"gl":["aluaces","pikamoku"],"hu":["jtg"],"it":["Damtux","ed0","FranzMari","noccio","nwandy","rickyroo","technezio"],"ja":["Naofumi"],"nb_NO":["elonus"],"nl":["davtemp","dehart","erikhubers","toonvangerwen","XtremeNova"],"pl":["gsz","mg6","oskarjakiela","TheName","TORminator"],"pt_BR":["amalvarenga","wanderlei.huttel"],"ru":["aigoshin","anm","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy"]}

View File

@@ -11,7 +11,6 @@ package at.bitfire.davdroid
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
@@ -22,8 +21,10 @@ import androidx.appcompat.content.res.AppCompatResources
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.concurrent.thread
import kotlin.system.exitProcess
@Suppress("unused")
@@ -73,17 +74,12 @@ class App: Application(), Thread.UncaughtExceptionHandler {
NotificationUtils.createChannels(this)
// don't block UI for some background checks
thread {
CoroutineScope(Dispatchers.Default).launch {
// watch installed/removed apps
val tasksFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
registerReceiver(PackageChangedReceiver(), tasksFilter)
OpenTasksWatcher(this@App)
// check whether a tasks app is currently installed
PackageChangedReceiver.updateTaskSync(this)
OpenTasksWatcher.updateTaskSync(this@App)
}
}

View File

@@ -28,12 +28,14 @@ import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
class DavService: android.app.Service() {
@@ -72,7 +74,9 @@ class DavService: android.app.Service() {
refreshingStatusListeners.forEach { listener ->
listener.get()?.onDavRefreshStatusChanged(id, true)
}
thread { refreshCollections(id) }
CoroutineScope(Dispatchers.IO).launch {
refreshCollections(id)
}
}
ACTION_FORCE_SYNC -> {

View File

@@ -0,0 +1,71 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class OpenTasksWatcher(
context: Context
): PackageChangedReceiver(context) {
companion object {
@WorkerThread
fun updateTaskSync(context: Context) {
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
Logger.log.info("App was launched or package was (in)installed; OpenTasks provider now available = $tasksInstalled")
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
val db = AppDatabase.getInstance(context)
db.serviceDao().getByType(Service.TYPE_CALDAV).forEach { service ->
val account = Account(service.accountName, context.getString(R.string.account_type))
val currentSyncable = ContentResolver.getIsSyncable(account, OpenTasks.authority)
var enabledAnyAccount = false
if (tasksInstalled) {
if (currentSyncable <= 0) {
Logger.log.info("Enabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
AccountSettings(context, account).setSyncInterval(OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL)
enabledAnyAccount = true
}
} else if (currentSyncable != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
if (enabledAnyAccount && !PermissionUtils.havePermissions(context, PermissionUtils.TASKS_PERMISSIONS)) {
Logger.log.warning("Tasks sync is now enabled for at least one account, but OpenTasks permissions are not granted")
PermissionUtils.notifyPermissions(context, null)
}
}
}
}
override fun onReceive(context: Context, intent: Intent) {
CoroutineScope(Dispatchers.Default).launch {
updateTaskSync(context)
}
}
}

View File

@@ -1,58 +1,25 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
import android.accounts.Account
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlin.concurrent.thread
import android.content.IntentFilter
class PackageChangedReceiver: BroadcastReceiver() {
abstract class PackageChangedReceiver(
val context: Context
): BroadcastReceiver(), AutoCloseable {
companion object {
@WorkerThread
fun updateTaskSync(context: Context) {
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
Logger.log.info("Tasks provider available = $tasksInstalled")
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
val db = AppDatabase.getInstance(context)
db.serviceDao().getByType(Service.TYPE_CALDAV).forEach { service ->
val account = Account(service.accountName, context.getString(R.string.account_type))
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
ContentResolver.addPeriodicSync(account, OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
}
} else
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
init {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
context.registerReceiver(this, filter)
}
override fun onReceive(context: Context, intent: Intent) {
thread {
updateTaskSync(context)
}
override fun close() {
context.unregisterReceiver(this)
}
}
}

View File

@@ -0,0 +1,72 @@
package at.bitfire.davdroid
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.PermissionsActivity
import at.bitfire.ical4android.TaskProvider
object PermissionUtils {
val CONTACT_PERMSSIONS = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
)
val CALENDAR_PERMISSIONS = arrayOf(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
val TASKS_PERMISSIONS = arrayOf(
TaskProvider.PERMISSION_READ_TASKS,
TaskProvider.PERMISSION_WRITE_TASKS
)
/**
* Checks whether at least one of the given permissions is granted.
*
* @param context context to check
* @param permissions array of permissions to check
*
* @return whether at least one of [permissions] is granted
*/
fun haveAnyPermission(context: Context, permissions: Array<String>) =
permissions.any { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
/**
* Checks whether all given permissions are granted.
*
* @param context context to check
* @param permissions array of permissions to check
*
* @return whether all [permissions] are granted
*/
fun havePermissions(context: Context, permissions: Array<String>) =
permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
/**
* Shows a notification about missing permissions.
*
* @param context notification context
* @param intent will be set as content Intent; if null, an Intent to launch PermissionsActivity will be used
*/
fun notifyPermissions(context: Context, intent: Intent?) {
val contentIntent = intent ?: Intent(context, PermissionsActivity::class.java)
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context)
.notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
}
}

View File

@@ -226,7 +226,10 @@ class LocalAddressBook(
}
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) <= 0)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY))
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
fun delete() {

View File

@@ -117,12 +117,16 @@ class LocalCalendar private constructor(
// get dirty events which are required to have an increased SEQUENCE value
for (localEvent in queryEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
val event = localEvent.event!!
val sequence = event.sequence
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (localEvent.weAreOrganizer)
event.sequence = sequence!! + 1
try {
val event = requireNotNull(localEvent.event)
val sequence = event.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (localEvent.weAreOrganizer) // increase sequence only if we're the organizer (i.e. not for attendee changes)
event.sequence = sequence + 1
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
dirty += localEvent
}

View File

@@ -20,7 +20,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
companion object {
init {
ICalendar.prodId = ProdId("+//IDN bitfire.at//${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ical4j/" + Constants.ical4jVersion)
ICalendar.prodId = ProdId("+//IDN bitfire.at//${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ical4j/" + Ical4Android.ical4jVersion)
}
const val COLUMN_ETAG = Events.SYNC_DATA1
@@ -52,7 +52,6 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
}
override fun populateEvent(row: ContentValues) {
super.populateEvent(row)
val event = requireNotNull(event)
event.uid = row.getAsString(Events.UID_2445)
@@ -60,6 +59,8 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
super.populateEvent(row)
}
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {

View File

@@ -20,7 +20,6 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import at.bitfire.dav4jvm.Constants
import at.bitfire.vcard4android.*
import java.util.*

View File

@@ -125,12 +125,16 @@ class LocalTaskList private constructor(
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks(Tasks._DIRTY, null)
for (localTask in tasks) {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else
task.sequence = sequence + 1
try {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else // task was modified, increase sequence
task.sequence = sequence + 1
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
}
return tasks
}

View File

@@ -34,7 +34,6 @@ import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
@@ -437,7 +436,7 @@ class AccountSettings(
@Suppress("unused")
private fun update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
PackageChangedReceiver.updateTaskSync(context)
OpenTasksWatcher.updateTaskSync(context)
}
@Suppress("unused")

View File

@@ -18,8 +18,10 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.concurrent.thread
/**
@@ -84,8 +86,8 @@ class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
override fun onAccountsUpdated(accounts: Array<out Account>?) {
thread {
cleanupAccounts(this)
CoroutineScope(Dispatchers.Default).launch {
cleanupAccounts(this@AccountAuthenticatorService)
}
}

View File

@@ -9,7 +9,10 @@ package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.content.*
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.ContactsContract
@@ -21,7 +24,6 @@ import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.account.AccountActivity
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.logging.Level
@@ -73,14 +75,8 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (remoteAddressBooks.isEmpty())
Logger.log.info("No contacts permission, but no address book selected for synchronization")
else {
// no contacts permission, but address books should be synchronized -> show notification
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
else
Logger.log.warning("No contacts permission, but address books are selected for synchronization")
return false
}

View File

@@ -27,7 +27,6 @@ import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.component.VAlarm
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@@ -36,6 +35,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.time.Duration
import java.util.*
import java.util.logging.Level
@@ -154,7 +154,7 @@ class CalendarSyncManager(
// set default reminder for non-full-day events, if requested
val defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
if (defaultAlarmMinBefore != null && !event.isAllDay() && event.alarms.isEmpty()) {
val alarm = VAlarm(Dur(0, 0, -defaultAlarmMinBefore, 0))
val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong()))
Logger.log.log(Level.FINE, "${event.uid}: Adding default alarm", alarm)
event.alarms += alarm
}

View File

@@ -10,7 +10,6 @@ package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.*
import android.content.pm.PackageManager
@@ -19,14 +18,10 @@ import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import at.bitfire.davdroid.R
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.account.SettingsActivity
import java.lang.ref.WeakReference
import java.util.*
@@ -98,7 +93,6 @@ abstract class SyncAdapterService: Service() {
}
}
abstract fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult)
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
@@ -114,11 +108,12 @@ abstract class SyncAdapterService: Service() {
runningSyncs += WeakReference(currentSync)
}
try {
// required for dav4jvm (ServiceLoader)
Thread.currentThread().contextClassLoader = context.classLoader
// required for ServiceLoader -> ical4j -> ical4android
Thread.currentThread().contextClassLoader = context.classLoader
sync(account, extras, authority, provider, syncResult)
try {
if (true)
sync(account, extras, authority, provider, syncResult)
} finally {
synchronized(runningSyncs) {
runningSyncs.removeAll { it.get() == null || it.get() == currentSync }
@@ -130,15 +125,19 @@ abstract class SyncAdapterService: Service() {
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
syncResult.databaseError = true
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
override fun onSyncCanceled() {
Logger.log.info("Sync thread cancelled! Interrupting sync")
super.onSyncCanceled()
}
override fun onSyncCanceled(thread: Thread) {
Logger.log.info("Sync thread ${thread.id} cancelled! Interrupting sync")
super.onSyncCanceled(thread)
}
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
if (settings.getSyncWifiOnly()) {
// WiFi required
@@ -173,8 +172,7 @@ abstract class SyncAdapterService: Service() {
val intent = Intent(context, SettingsActivity::class.java)
intent.putExtra(SettingsActivity.EXTRA_ACCOUNT, settings.account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
PermissionUtils.notifyPermissions(context, intent)
}
val wifi = context.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
@@ -188,18 +186,6 @@ abstract class SyncAdapterService: Service() {
return true
}
protected fun notifyPermissions(intent: Intent) {
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
}
}
}

View File

@@ -27,7 +27,6 @@ import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.GetETag
import at.bitfire.dav4jvm.property.SyncToken
import at.bitfire.davdroid.*
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.*
@@ -36,8 +35,14 @@ import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.account.SettingsActivity
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.UsesThreadContextClassLoader
import at.bitfire.vcard4android.ContactsStorageException
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.lang3.exception.ContextedException
@@ -47,13 +52,17 @@ import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.security.cert.CertificateException
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
import kotlin.math.min
@Suppress("MemberVisibilityCanBePrivate")
@UsesThreadContextClassLoader
abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
val context: Context,
val account: Account,
@@ -70,18 +79,21 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
companion object {
val MAX_PROCESSING_THREADS = // nCPU/2 (rounded up for case of 1 CPU), but max. 4
min((Runtime.getRuntime().availableProcessors()+1)/2, 4)
val MAX_DOWNLOAD_THREADS = // one (if one CPU), 2 otherwise
min(Runtime.getRuntime().availableProcessors(), 2)
const val MAX_MULTIGET_RESOURCES = 10
}
init {
Logger.log.info("SyncManager: using up to $MAX_PROCESSING_THREADS processing threads and $MAX_DOWNLOAD_THREADS download threads")
// required for ServiceLoader -> ical4j -> ical4android
Ical4Android.checkThreadContextClassLoader()
}
/**
* We use our own dispatcher to make sure that all threads have [Thread.getContextClassLoader] set,
* which is required for dav4jvm and ical4j (because they rely on [ServiceLoader]).
*/
private val workDispatcher = Executors.newFixedThreadPool(
// number of threads = number of CPUs, but max. 4
min(Runtime.getRuntime().availableProcessors(), 4)
).asCoroutineDispatcher()
private val mainAccount = if (localCollection is LocalAddressBook)
localCollection.mainAccount
@@ -113,16 +125,13 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
Logger.log.info("No reason to synchronize, aborting")
return@unwrapExceptions
}
abortIfCancelled()
Logger.log.info("Querying server capabilities")
var remoteSyncState = queryCapabilities()
abortIfCancelled()
Logger.log.info("Sending local deletes/updates to server")
val modificationsSent = processLocallyDeleted() ||
uploadDirty()
abortIfCancelled()
if (extras.containsKey(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC)) {
Logger.log.info("Forcing re-synchronization of all entries")
@@ -276,7 +285,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
// but only if they don't have changed on the server. Then finally remove them from the local address book.
val localList = localCollection.findDeleted()
for (local in localList) {
abortIfCancelled()
useLocal(local) {
val fileName = local.fileName
if (fileName != null) {
@@ -315,56 +323,58 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
local.assignNameAndUID()
}
// upload dirty resources
for (local in localCollection.findDirty())
useLocal(local) {
abortIfCancelled()
// upload dirty resources (parallelized)
runBlocking(workDispatcher) {
for (local in localCollection.findDirty())
launch {
useLocal(local) {
val fileName = local.fileName!!
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
// generate entity to upload (VCard, iCal, whatever)
val body = prepareUpload(local)
val fileName = local.fileName!!
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
// generate entity to upload (VCard, iCal, whatever)
val body = prepareUpload(local)
var eTag: String? = null
val processETag: (response: okhttp3.Response) -> Unit = { response ->
response.header("ETag")?.let { getETag ->
eTag = GetETag(getETag).eTag
}
}
try {
if (local.eTag == null) {
Logger.log.info("Uploading new record $fileName")
remote.put(body, null, true, processETag)
} else {
Logger.log.info("Uploading locally modified record $fileName")
remote.put(body, local.eTag, false, processETag)
}
numUploaded++
} catch(e: ForbiddenException) {
// HTTP 403 Forbidden
// If and only if the upload failed because of missing permissions, treat it like 412.
if (e.errors.contains(Error.NEED_PRIVILEGES))
Logger.log.log(Level.INFO, "Couldn't upload because of missing permissions, ignoring", e)
else
throw e
} catch(e: ConflictException) {
// HTTP 409 Conflict
// We can't interact with the user to resolve the conflict, so we treat 409 like 412.
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
// HTTP 412 Precondition failed: Resource has been modified on the server in the meanwhile.
// Ignore this condition so that the resource can be downloaded and reset again.
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
var eTag: String? = null
val processETag: (response: okhttp3.Response) -> Unit = { response ->
response.header("ETag")?.let { getETag ->
eTag = GetETag(getETag).eTag
if (eTag != null)
Logger.log.fine("Received new ETag=$eTag after uploading")
else
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
local.clearDirty(eTag)
}
}
try {
if (local.eTag == null) {
Logger.log.info("Uploading new record $fileName")
remote.put(body, null, true, processETag)
} else {
Logger.log.info("Uploading locally modified record $fileName")
remote.put(body, local.eTag, false, processETag)
}
numUploaded++
} catch(e: ForbiddenException) {
// HTTP 403 Forbidden
// If and only if the upload failed because of missing permissions, treat it like 412.
if (e.errors.contains(Error.NEED_PRIVILEGES))
Logger.log.log(Level.INFO, "Couldn't upload because of missing permissions, ignoring", e)
else
throw e
} catch(e: ConflictException) {
// HTTP 409 Conflict
// We can't interact with the user to resolve the conflict, so we treat 409 like 412.
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
// HTTP 412 Precondition failed: Resource has been modified on the server in the meanwhile.
// Ignore this condition so that the resource can be downloaded and reset again.
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
if (eTag != null)
Logger.log.fine("Received new ETag=$eTag after uploading")
else
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
local.clearDirty(eTag)
}
}
}
Logger.log.info("Sent $numUploaded record(s) to server")
return numUploaded > 0
}
@@ -437,116 +447,87 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
* @param listRemote function to list remote resources (for instance, all since a certain sync-token)
*/
protected open fun syncRemote(listRemote: (DavResponseCallback) -> Unit) {
// results must be processed in main thread because exceptions must be thrown in main
// thread, so that they can be catched by SyncManager
val results = ConcurrentLinkedQueue<Future<*>>()
// thread-safe sync stats
val nInserted = AtomicInteger()
val nUpdated = AtomicInteger()
val nDeleted = AtomicInteger()
val nSkipped = AtomicInteger()
// download queue
val toDownload = LinkedBlockingQueue<HttpUrl>()
runBlocking(workDispatcher) {
// download queue
val toDownload = LinkedBlockingQueue<HttpUrl>()
fun download(url: HttpUrl?) {
if (url != null)
toDownload += url
// tasks from this executor create the download tasks (if necessary)
val processor = ThreadPoolExecutor(1, MAX_PROCESSING_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_PROCESSING_THREADS), // accept up to MAX_PROCESSING_THREADS processing tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
// this executor runs the actual download tasks
val downloader = ThreadPoolExecutor(0, MAX_DOWNLOAD_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_DOWNLOAD_THREADS), // accept up to MAX_DOWNLOAD_THREADS download tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
fun downloadBunch() {
val bunch = LinkedList<HttpUrl>()
toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES)
results += downloader.submit {
downloadRemote(bunch)
}
}
listRemote { response, relation ->
// ignore non-members
if (relation != Response.HrefRelation.MEMBER)
return@listRemote
// ignore collections
if (response[at.bitfire.dav4jvm.property.ResourceType::class.java]?.types?.contains(at.bitfire.dav4jvm.property.ResourceType.COLLECTION) == true)
return@listRemote
val name = response.hrefName()
if (response.isSuccess()) {
Logger.log.fine("Found remote resource: $name")
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
if (local == null) {
Logger.log.info("$name has been added remotely")
toDownload += response.href
nInserted.incrementAndGet()
} else {
val localETag = local.eTag
val remoteETag = response[GetETag::class.java]?.eTag ?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag) {
Logger.log.info("$name has not been changed on server (ETag still $remoteETag)")
nSkipped.incrementAndGet()
} else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
toDownload += response.href
nUpdated.incrementAndGet()
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
if (toDownload.size >= MAX_MULTIGET_RESOURCES || url == null) {
while (toDownload.size > 0) {
val bunch = LinkedList<HttpUrl>()
toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES)
launch {
downloadRemote(bunch)
}
}
synchronized(processor) {
if (toDownload.size >= MAX_MULTIGET_RESOURCES)
// download another bunch of MAX_MULTIGET_RESOURCES resources
downloadBunch()
}
}
}
} else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) {
// collection sync: resource has been deleted on remote server
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
Logger.log.info("$name has been deleted on server, deleting locally")
local?.delete()
nDeleted.incrementAndGet()
coroutineScope {
listRemote { response, relation ->
// ignore non-members
if (relation != Response.HrefRelation.MEMBER)
return@listRemote
// ignore collections
if (response[at.bitfire.dav4jvm.property.ResourceType::class.java]?.types?.contains(at.bitfire.dav4jvm.property.ResourceType.COLLECTION) == true)
return@listRemote
val name = response.hrefName()
if (response.isSuccess()) {
Logger.log.fine("Found remote resource: $name")
launch {
useLocal(localCollection.findByName(name)) { local ->
if (local == null) {
Logger.log.info("$name has been added remotely, queueing download")
download(response.href)
nInserted.incrementAndGet()
} else {
val localETag = local.eTag
val remoteETag = response[GetETag::class.java]?.eTag
?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag) {
Logger.log.info("$name has not been changed on server (ETag still $remoteETag)")
nSkipped.incrementAndGet()
} else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
download(response.href)
nUpdated.incrementAndGet()
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
}
}
}
} else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) {
// collection sync: resource has been deleted on remote server
launch {
useLocal(localCollection.findByName(name)) { local ->
Logger.log.info("$name has been deleted on server, deleting locally")
local?.delete()
nDeleted.incrementAndGet()
}
}
}
}
}
// check already available results for exceptions so that they don't become too many
checkResults(results)
// download remaining resources
download(null)
}
// process remaining responses
processor.shutdown()
if (!processor.awaitTermination(5, TimeUnit.MINUTES))
throw TimeoutException("Processing the remote resource list took too long")
// download remaining resources
if (toDownload.isNotEmpty())
downloadBunch()
// signal end of queue and wait for download thread
downloader.shutdown()
if (!downloader.awaitTermination(5, TimeUnit.MINUTES))
throw TimeoutException("Downloading and processing the remote resources took too long")
// check remaining results for exceptions
checkResults(results)
// update sync stats
with(syncResult.stats) {
numInserts += nInserted.get()
@@ -629,17 +610,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
// sync helpers
/**
* Throws an [InterruptedException] if the current thread has been interrupted,
* most probably because synchronization was cancelled by the user.
*
* @throws InterruptedException (which will be caught by [performSync])
* */
protected fun abortIfCancelled() {
if (Thread.interrupted())
throw InterruptedException("Sync was cancelled")
}
protected fun syncState(dav: Response) =
dav[SyncToken::class.java]?.token?.let {
SyncState(SyncState.Type.SYNC_TOKEN, it)
@@ -797,6 +767,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
@Deprecated("Use Kotlin coroutines instead")
fun checkResults(results: MutableCollection<Future<*>>) {
val iter = results.iterator()
while (iter.hasNext()) {

View File

@@ -28,7 +28,6 @@ import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.Constants
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.Task
import okhttp3.HttpUrl
@@ -115,7 +114,7 @@ class TasksSyncManager(
override fun postProcess() {
val touched = localCollection.touchRelations()
Constants.log.info("Touched $touched relations")
Logger.log.info("Touched $touched relations")
}
// helpers

View File

@@ -26,6 +26,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import at.bitfire.davdroid.App
@@ -36,12 +37,13 @@ import kotlinx.android.synthetic.main.about.*
import kotlinx.android.synthetic.main.about_languages.*
import kotlinx.android.synthetic.main.about_translation.view.*
import kotlinx.android.synthetic.main.activity_about.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import org.json.JSONObject
import java.text.Collator
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
class AboutActivity: AppCompatActivity() {
@@ -108,7 +110,7 @@ class AboutActivity: AppCompatActivity() {
inflater.inflate(R.layout.about, container, false)!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
app_name.text = getString(R.string.app_name)
app_name.setText(R.string.app_name)
app_version.text = getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
build_time.text = getString(R.string.about_build_date, SimpleDateFormat.getDateInstance().format(BuildConfig.buildTime))
@@ -212,8 +214,9 @@ class AboutActivity: AppCompatActivity() {
@UiThread
fun initialize(assetName: String, html: Boolean) {
if (initialized) return
initialized = true
thread {
viewModelScope.launch(Dispatchers.IO) {
getApplication<Application>().resources.assets.open(assetName).use {
val raw = IOUtils.toString(it, Charsets.UTF_8)
if (html) {
@@ -223,8 +226,6 @@ class AboutActivity: AppCompatActivity() {
plainText.postValue(raw)
}
}
initialized = true
}
}

View File

@@ -27,8 +27,10 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.accounts_content.*
import kotlinx.android.synthetic.main.activity_accounts.*
import kotlinx.android.synthetic.main.activity_accounts.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.concurrent.thread
class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver {
@@ -49,10 +51,10 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
settings = Settings.getInstance(this)
if (savedInstanceState == null)
thread {
CoroutineScope(Dispatchers.Default).launch {
// use a separate thread to check whether IntroActivity should be shown
if (IntroActivity.shouldShowIntroActivity(this)) {
val intro = Intent(this, IntroActivity::class.java)
if (IntroActivity.shouldShowIntroActivity(this@AccountsActivity)) {
val intro = Intent(this@AccountsActivity, IntroActivity::class.java)
startActivityForResult(intro, REQUEST_INTRO)
}
}

View File

@@ -21,7 +21,6 @@ import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment
import at.bitfire.davdroid.ui.intro.IntroActivity
import at.bitfire.davdroid.ui.intro.OpenSourceFragment
import at.bitfire.davdroid.ui.intro.OpenTasksFragment
import com.google.android.material.snackbar.Snackbar

View File

@@ -21,15 +21,17 @@ import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityCreateAddressBookBinding
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.ui.account.AccountActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.lang3.StringUtils
import java.util.*
import kotlin.concurrent.thread
class CreateAddressBookActivity: AppCompatActivity() {
@@ -119,7 +121,7 @@ class CreateAddressBookActivity: AppCompatActivity() {
return
this.account = account
thread {
viewModelScope.launch(Dispatchers.IO) {
// load account info
val adapter = HomeSetAdapter(getApplication())

View File

@@ -25,6 +25,7 @@ import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityCreateCalendarBinding
@@ -36,10 +37,11 @@ import at.bitfire.ical4android.DateUtils
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import kotlinx.android.synthetic.main.activity_create_calendar.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.fortuna.ical4j.model.Calendar
import org.apache.commons.lang3.StringUtils
import java.util.*
import kotlin.concurrent.thread
class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
@@ -228,7 +230,7 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
supportVTODO.value = true
supportVJOURNAL.value = true
thread {
viewModelScope.launch(Dispatchers.IO) {
// load account info
val adapter = HomeSetAdapter(getApplication())

View File

@@ -25,11 +25,13 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.settings.AccountSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.IOException
import java.io.StringWriter
import java.util.logging.Level
import kotlin.concurrent.thread
class CreateCollectionFragment: DialogFragment() {
@@ -101,8 +103,8 @@ class CreateCollectionFragment: DialogFragment() {
class Model(
application: Application
): AndroidViewModel(application) {
app: Application
): AndroidViewModel(app) {
lateinit var account: Account
lateinit var serviceType: String
@@ -111,7 +113,7 @@ class CreateCollectionFragment: DialogFragment() {
val result = MutableLiveData<Exception>()
fun createCollection(): LiveData<Exception> {
thread {
viewModelScope.launch(Dispatchers.IO + NonCancellable) {
HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account))
.setForeground(true)
.build().use { httpClient ->

View File

@@ -37,6 +37,7 @@ import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.InvalidAccountException
@@ -47,13 +48,14 @@ import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.TaskProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.lang3.exception.ExceptionUtils
import org.dmfs.tasks.contract.TaskContract
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.util.logging.Level
import kotlin.concurrent.thread
class DebugInfoActivity: AppCompatActivity() {
@@ -128,7 +130,7 @@ class DebugInfoActivity: AppCompatActivity() {
Logger.log.info("Generating debug info report")
initialized = true
thread {
viewModelScope.launch(Dispatchers.Default) {
val context = getApplication<Application>()
val text = StringBuilder("--- BEGIN DEBUG INFO ---\n")
@@ -301,7 +303,9 @@ class DebugInfoActivity: AppCompatActivity() {
accountSettings.getSyncWifiOnlySSIDs()?.let {
text.append(", SSIDs: ${accountSettings.getSyncWifiOnlySSIDs()}")
}
text.append("\n [CardDAV] Contact group method: ${accountSettings.getGroupMethod()}")
text .append("\n getIsSyncable(CalendarContract): ${ContentResolver.getIsSyncable(acct, CalendarContract.AUTHORITY)}")
.append("\n getIsSyncable(OpenTasks): ${ContentResolver.getIsSyncable(acct, TaskProvider.ProviderName.OpenTasks.authority)}")
.append("\n [CardDAV] Contact group method: ${accountSettings.getGroupMethod()}")
.append("\n [CalDAV] Time range (past days): ${accountSettings.getTimeRangePastDays()}")
.append("\n Manage calendar colors: ${accountSettings.getManageCalendarColors()}")
.append("\n Use event colors: ${accountSettings.getEventColors()}")

View File

@@ -29,6 +29,8 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
override fun initMenu(context: Context, menu: Menu) {
if (BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
menu.findItem(R.id.nav_beta_feedback).isVisible = true
if (/* ose */ true)
menu.findItem(R.id.nav_donate).isVisible = true
}
override fun onNavigationItemSelected(activity: Activity, item: MenuItem): Boolean {

View File

@@ -23,7 +23,9 @@ import at.bitfire.davdroid.databinding.DeleteCollectionBinding
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.settings.AccountSettings
import kotlin.concurrent.thread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
class DeleteCollectionFragment: DialogFragment() {
@@ -97,15 +99,15 @@ class DeleteCollectionFragment: DialogFragment() {
this.account = account
if (collectionInfo == null)
thread {
viewModelScope.launch(Dispatchers.IO) {
collectionInfo = db.collectionDao().get(collectionId)
}
}
fun deleteCollection(): LiveData<Exception> {
thread {
val account = account ?: return@thread
val collectionInfo = collectionInfo ?: return@thread
viewModelScope.launch(Dispatchers.IO + NonCancellable) {
val account = account ?: return@launch
val collectionInfo = collectionInfo ?: return@launch
val context = getApplication<Application>()
HttpClient.Builder(context, AccountSettings(context, account))

View File

@@ -0,0 +1,17 @@
package at.bitfire.davdroid.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class PermissionsActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null)
supportFragmentManager.beginTransaction()
.add(android.R.id.content, PermissionsFragment())
.commit()
}
}

View File

@@ -0,0 +1,136 @@
package at.bitfire.davdroid.ui
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.PackageChangedReceiver
import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS
import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMSSIONS
import at.bitfire.davdroid.PermissionUtils.TASKS_PERMISSIONS
import at.bitfire.davdroid.PermissionUtils.havePermissions
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityPermissionsBinding
import at.bitfire.davdroid.resource.LocalTaskList
class PermissionsFragment: Fragment() {
lateinit var model: Model
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProvider(this).get(Model::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = ActivityPermissionsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.model = model
binding.text.text = getString(R.string.permissions_text, getString(R.string.app_name))
model.needContactsPermissions.observe(viewLifecycleOwner, Observer { needContacts ->
if (needContacts && model.haveContactsPermissions.value == false)
requestPermissions(CONTACT_PERMSSIONS, 0)
})
model.needCalendarPermissions.observe(viewLifecycleOwner, Observer { needCalendars ->
if (needCalendars && model.haveCalendarPermissions.value == false)
requestPermissions(CALENDAR_PERMISSIONS, 0)
})
model.needTasksPermissions.observe(viewLifecycleOwner, Observer { needTasks ->
if (needTasks == true && model.haveTasksPermissions.value == false)
requestPermissions(TASKS_PERMISSIONS, 0)
})
model.needAllPermissions.observe(viewLifecycleOwner, Observer { needAll ->
if (needAll && model.haveAllPermissions.value == false) {
val all = CONTACT_PERMSSIONS + CALENDAR_PERMISSIONS +
if (model.haveTasksPermissions.value != null) TASKS_PERMISSIONS else emptyArray()
requestPermissions(all, 0)
}
})
binding.appSettings.setOnClickListener {
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null))
if (intent.resolveActivity(requireActivity().packageManager) != null)
startActivity(intent)
}
return binding.root
}
override fun onResume() {
super.onResume()
model.checkPermissions()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
model.checkPermissions()
}
class Model(app: Application): AndroidViewModel(app) {
val haveContactsPermissions = MutableLiveData<Boolean>()
val needContactsPermissions = MutableLiveData<Boolean>()
val haveCalendarPermissions = MutableLiveData<Boolean>()
val needCalendarPermissions = MutableLiveData<Boolean>()
val haveTasksPermissions = MutableLiveData<Boolean>()
val needTasksPermissions = MutableLiveData<Boolean>()
val tasksWatcher = object: PackageChangedReceiver(app) {
override fun onReceive(context: Context?, intent: Intent?) {
checkPermissions()
}
}
val haveAllPermissions = MutableLiveData<Boolean>()
val needAllPermissions = MutableLiveData<Boolean>()
init {
checkPermissions()
}
override fun onCleared() {
tasksWatcher.close()
}
fun checkPermissions() {
val contactPermissions = havePermissions(getApplication(), CONTACT_PERMSSIONS)
haveContactsPermissions.value = contactPermissions
needContactsPermissions.value = contactPermissions
val calendarPermissions = havePermissions(getApplication(), CALENDAR_PERMISSIONS)
haveCalendarPermissions.value = calendarPermissions
needCalendarPermissions.value = calendarPermissions
val tasksAvailable = LocalTaskList.tasksProviderAvailable(getApplication())
var tasksPermissions: Boolean? = null
if (tasksAvailable) {
tasksPermissions = havePermissions(getApplication(), TASKS_PERMISSIONS)
haveTasksPermissions.value = tasksPermissions
needTasksPermissions.value = tasksPermissions
} else {
haveTasksPermissions.value = null
needTasksPermissions.value = null
}
val allPermissions = contactPermissions && calendarPermissions && (!tasksAvailable || tasksPermissions == true)
haveAllPermissions.value = allPermissions
needAllPermissions.value = allPermissions
}
}
}

View File

@@ -1,22 +1,18 @@
package at.bitfire.davdroid.ui.account
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.annotation.MainThread
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.lifecycle.*
@@ -26,14 +22,14 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider
import at.bitfire.davdroid.ui.PermissionsActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_account.*
import java.util.concurrent.Executors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.concurrent.thread
class AccountActivity: AppCompatActivity() {
@@ -50,7 +46,7 @@ class AccountActivity: AppCompatActivity() {
model = ViewModelProvider(this).get(Model::class.java)
(intent.getParcelableExtra(EXTRA_ACCOUNT) as? Account)?.let { account ->
model.initialize(account)
}
} ?: throw IllegalArgumentException("AccountActivity requires EXTRA_ACCOUNT")
title = model.account.name
setContentView(R.layout.activity_account)
@@ -67,11 +63,6 @@ class AccountActivity: AppCompatActivity() {
tabsAdapter.calDavSvcId = it
})
model.askForPermissions.observe(this, Observer { permissions ->
if (permissions.isNotEmpty())
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 0)
})
sync.setOnClickListener {
DavUtils.requestSync(this, model.account)
Snackbar.make(view_pager, R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
@@ -83,11 +74,6 @@ class AccountActivity: AppCompatActivity() {
return true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults.contains(PackageManager.PERMISSION_GRANTED))
model.gotPermissions()
}
// menu actions
@@ -141,11 +127,19 @@ class AccountActivity: AppCompatActivity() {
}
// other actions
fun startPermissionsActivity(view: View) {
startActivity(Intent(this, PermissionsActivity::class.java))
}
// adapter
class TabsAdapter(
val activity: AppCompatActivity
): FragmentStatePagerAdapter(activity.supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
): FragmentStatePagerAdapter(activity.supportFragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var cardDavSvcId: Long? = null
set(value) {
@@ -234,22 +228,10 @@ class AccountActivity: AppCompatActivity() {
private set
private val db = AppDatabase.getInstance(application)
private val executor = Executors.newSingleThreadExecutor()
val cardDavService = MutableLiveData<Long>()
val calDavService = MutableLiveData<Long>()
private val needContactPermissions: LiveData<Boolean> = Transformations.switchMap(cardDavService) { cardDavId ->
if (cardDavId != null)
db.collectionDao().observeHasSyncByService(cardDavId)
else
MutableLiveData<Boolean>().apply { value = false }
}
private val needCalendarPermissions: LiveData<Boolean> = Transformations.map(calDavService) { calDavId ->
calDavId != null
}
val askForPermissions = PermissionCalculator(application, needContactPermissions, needCalendarPermissions)
@MainThread
fun initialize(account: Account) {
@@ -259,85 +241,24 @@ class AccountActivity: AppCompatActivity() {
this.account = account
thread {
viewModelScope.launch(Dispatchers.IO) {
cardDavService.postValue(db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CARDDAV))
calDavService.postValue(db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CALDAV))
}
}
fun gotPermissions() {
askForPermissions.calculate()
}
fun toggleSync(item: Collection) =
executor.execute {
val newItem = item.copy(sync = !item.sync)
db.collectionDao().update(newItem)
}
fun toggleReadOnly(item: Collection) =
executor.execute {
val newItem = item.copy(forceReadOnly = !item.forceReadOnly)
db.collectionDao().update(newItem)
}
}
class PermissionCalculator(
val context: Context,
needContactPermissions: LiveData<Boolean>,
needCalendarPermissions: LiveData<Boolean>
): MediatorLiveData<List<String>>() {
companion object {
val contactPermissions = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
)
val calendarPermissions = arrayOf(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
val taskPermissions = arrayOf(
TaskProvider.PERMISSION_READ_TASKS,
TaskProvider.PERMISSION_WRITE_TASKS
)
}
private var usesContacts: Boolean? = null
private var usesCalendars: Boolean? = null
init {
addSource(needContactPermissions) {
usesContacts = it
calculate()
}
addSource(needCalendarPermissions) {
usesCalendars = it
calculate()
fun toggleSync(item: Collection) {
viewModelScope.launch(Dispatchers.IO + NonCancellable) {
val newItem = item.copy(sync = !item.sync)
db.collectionDao().update(newItem)
}
}
fun calculate() {
val contacts = usesContacts ?: return
val calendar = usesCalendars ?: return
val required = mutableListOf<String>()
if (contacts)
required.addAll(contactPermissions)
if (calendar) {
required.addAll(calendarPermissions)
if (LocalTaskList.tasksProviderAvailable(context))
required.addAll(taskPermissions)
fun toggleReadOnly(item: Collection) {
viewModelScope.launch(Dispatchers.IO + NonCancellable) {
val newItem = item.copy(forceReadOnly = !item.forceReadOnly)
db.collectionDao().update(newItem)
}
// only ask for permissions which are not granted
val askFor = required.filter {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_DENIED
}
if (value != askFor)
value = askFor
}
}

View File

@@ -2,10 +2,12 @@ package at.bitfire.davdroid.ui.account
import android.content.Intent
import android.view.*
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.ui.CreateAddressBookActivity
import kotlinx.android.synthetic.main.account_carddav_item.view.*
import kotlinx.android.synthetic.main.account_collections.*
class AddressBooksFragment: CollectionsFragment() {
@@ -28,6 +30,15 @@ class AddressBooksFragment: CollectionsFragment() {
return false
}
override fun checkPermissions() {
if (PermissionUtils.havePermissions(requireActivity(), PermissionUtils.CONTACT_PERMSSIONS))
permissionsCard.visibility = View.GONE
else {
permissionsText.setText(R.string.account_carddav_missing_permissions)
permissionsCard.visibility = View.VISIBLE
}
}
override fun createAdapter() = AddressBookAdapter(accountModel)

View File

@@ -3,10 +3,13 @@ package at.bitfire.davdroid.ui.account
import android.content.Intent
import android.view.*
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.ui.CreateCalendarActivity
import kotlinx.android.synthetic.main.account_caldav_item.view.*
import kotlinx.android.synthetic.main.account_collections.*
class CalendarsFragment: CollectionsFragment() {
@@ -29,6 +32,22 @@ class CalendarsFragment: CollectionsFragment() {
return false
}
override fun checkPermissions() {
val calendarPermissions = PermissionUtils.havePermissions(requireActivity(), PermissionUtils.CALENDAR_PERMISSIONS)
val tasksPermissions = !LocalTaskList.tasksProviderAvailable(requireActivity()) ||
PermissionUtils.havePermissions(requireActivity(), PermissionUtils.TASKS_PERMISSIONS)
if (calendarPermissions && tasksPermissions)
permissionsCard.visibility = View.GONE
else {
permissionsText.setText(when {
!calendarPermissions && tasksPermissions -> R.string.account_caldav_missing_calendar_permissions
calendarPermissions && !tasksPermissions -> R.string.account_caldav_missing_tasks_permissions
else -> R.string.account_caldav_missing_permissions
})
permissionsCard.visibility = View.VISIBLE
}
}
override fun createAdapter(): CollectionAdapter = CalendarAdapter(accountModel)

View File

@@ -18,10 +18,12 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.databinding.CollectionPropertiesBinding
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import kotlin.concurrent.thread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class CollectionInfoFragment: DialogFragment() {
@@ -67,7 +69,7 @@ class CollectionInfoFragment: DialogFragment() {
return
initialized = true
thread {
viewModelScope.launch(Dispatchers.IO) {
val db = AppDatabase.getInstance(getApplication())
collection.postValue(db.collectionDao().get(collectionId))
}

View File

@@ -29,7 +29,8 @@ import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.ui.DeleteCollectionFragment
import at.bitfire.ical4android.TaskProvider
import kotlinx.android.synthetic.main.account_collections.*
import java.util.concurrent.Executors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshListener {
@@ -112,8 +113,6 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
no_collections.setText(noCollectionsStringId)
}
protected abstract fun createAdapter(): CollectionAdapter
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.refresh -> {
@@ -128,6 +127,13 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
model.refresh()
}
override fun onResume() {
super.onResume()
checkPermissions()
}
protected abstract fun checkPermissions()
protected abstract fun createAdapter(): CollectionAdapter
abstract class CollectionViewHolder(
@@ -207,7 +213,6 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
class Model(application: Application): AndroidViewModel(application), DavService.RefreshingStatusListener, SyncStatusObserver {
private val db = AppDatabase.getInstance(application)
private val executor = Executors.newSingleThreadExecutor()
private lateinit var accountModel: AccountActivity.Model
val serviceId = MutableLiveData<Long>()
@@ -250,8 +255,8 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
if (context.bindService(Intent(context, DavService::class.java), svcConn, Context.BIND_AUTO_CREATE))
davServiceConn = svcConn
executor.submit {
syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
viewModelScope.launch(Dispatchers.Default) {
syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model)
checkSyncStatus()
}
}
@@ -281,7 +286,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
}
override fun onStatusChanged(which: Int) {
executor.submit {
viewModelScope.launch(Dispatchers.Default) {
checkSyncStatus()
}
}

View File

@@ -11,18 +11,15 @@ import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.widget.EditText
import android.widget.LinearLayout
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.*
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.closeCompat
@@ -33,8 +30,10 @@ import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.TaskProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.concurrent.thread
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class RenameAccountFragment: DialogFragment() {
@@ -77,11 +76,12 @@ class RenameAccountFragment: DialogFragment() {
.setView(layout)
.setPositiveButton(R.string.account_rename_rename, DialogInterface.OnClickListener { _, _ ->
val newName = editText.text.toString()
if (newName == oldAccount.name)
return@OnClickListener
model.renameAccount(oldAccount, newName)
requireActivity().finish()
})
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
@@ -97,26 +97,25 @@ class RenameAccountFragment: DialogFragment() {
fun renameAccount(oldAccount: Account, newName: String) {
val context = getApplication<Application>()
thread {
// remember sync intervals
val oldSettings = AccountSettings(context, oldAccount)
val authorities = arrayOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.OpenTasks.authority
)
val syncIntervals = authorities.map { Pair(it, oldSettings.getSyncInterval(it)) }
// remember sync intervals
val oldSettings = AccountSettings(context, oldAccount)
val authorities = arrayOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.OpenTasks.authority
)
val syncIntervals = authorities.map { Pair(it, oldSettings.getSyncInterval(it)) }
val accountManager = AccountManager.get(context)
accountManager.renameAccount(oldAccount, newName, {
thread {
onAccountRenamed(accountManager, oldAccount, newName, syncIntervals)
}
}, null)
}
val accountManager = AccountManager.get(context)
accountManager.renameAccount(oldAccount, newName, {
viewModelScope.launch(Dispatchers.Default + NonCancellable) {
onAccountRenamed(accountManager, oldAccount, newName, syncIntervals)
}
}, null)
}
@SuppressLint("Recycle")
@WorkerThread
fun onAccountRenamed(accountManager: AccountManager, oldAccount: Account, newName: String, syncIntervals: List<Pair<String, Long?>>) {
// account has now been renamed
Logger.log.info("Updating account name references")
@@ -129,8 +128,6 @@ class RenameAccountFragment: DialogFragment() {
// update account name references in database
val db = AppDatabase.getInstance(context)
Logger.log.log(Level.INFO, "Main thread", Looper.getMainLooper().thread)
Logger.log.log(Level.INFO, "Current thread", Thread.currentThread())
db.serviceDao().renameAccount(oldAccount.name, newName)
// update main account of address book accounts

View File

@@ -4,7 +4,6 @@ import android.Manifest
import android.app.Activity
import android.app.Application
import android.content.ContentProviderClient
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.ContentObserver
@@ -20,10 +19,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.lifecycle.*
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.room.Transaction
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.closeCompat
import at.bitfire.davdroid.log.Logger
@@ -31,9 +34,13 @@ import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.account_caldav_item.view.*
import kotlinx.android.synthetic.main.account_collections.*
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.util.logging.Level
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
class WebcalFragment: CollectionsFragment() {
@@ -45,10 +52,6 @@ class WebcalFragment: CollectionsFragment() {
super.onCreate(savedInstanceState)
webcalModel = ViewModelProvider(this).get(WebcalModel::class.java)
webcalModel.calendarPermission.observe(this, Observer { granted ->
if (!granted)
requestPermissions(arrayOf(Manifest.permission.READ_CALENDAR), 0)
})
webcalModel.subscribedUrls.observe(this, Observer { urls ->
Logger.log.log(Level.FINE, "Got Android calendar list", urls.keys)
})
@@ -56,9 +59,6 @@ class WebcalFragment: CollectionsFragment() {
webcalModel.initialize(arguments?.getLong(EXTRA_SERVICE_ID) ?: throw IllegalArgumentException("EXTRA_SERVICE_ID required"))
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) =
webcalModel.calendarPermission.check()
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.caldav_actions, menu)
@@ -68,6 +68,15 @@ class WebcalFragment: CollectionsFragment() {
}
override fun checkPermissions() {
if (PermissionUtils.havePermissions(requireActivity(), PermissionUtils.CALENDAR_PERMISSIONS))
permissionsCard.visibility = View.GONE
else {
permissionsText.setText(R.string.account_webcal_missing_calendar_permissions)
permissionsCard.visibility = View.VISIBLE
}
}
override fun createAdapter(): CollectionAdapter = WebcalAdapter(accountModel, webcalModel)
@@ -159,18 +168,16 @@ class WebcalFragment: CollectionsFragment() {
private val db = AppDatabase.getInstance(application)
private val resolver = application.contentResolver
val calendarPermission = CalendarPermission(application)
private var calendarPermission = false
private val calendarProvider = object: MediatorLiveData<ContentProviderClient>() {
var havePermission = false
init {
addSource(calendarPermission) { granted ->
havePermission = granted
if (granted)
connect()
else
disconnect()
}
init()
}
fun init() {
calendarPermission = ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED
if (calendarPermission)
connect()
}
override fun onActive() {
@@ -179,7 +186,7 @@ class WebcalFragment: CollectionsFragment() {
}
fun connect() {
if (havePermission && value == null)
if (calendarPermission && value == null)
value = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
}
@@ -274,13 +281,12 @@ class WebcalFragment: CollectionsFragment() {
initialized = true
serviceId = dbServiceId
calendarPermission.check()
}
fun unsubscribe(webcal: Collection) {
workerHandler.post {
// find first matching source (Webcal) URL
subscribedUrls.value?.entries?.firstOrNull { (id, source) ->
subscribedUrls.value?.entries?.firstOrNull { (_, source) ->
UrlUtils.equals(source, webcal.source!!)
}?.key?.let { id ->
// delete first matching subscription from Android calendar list
@@ -292,14 +298,4 @@ class WebcalFragment: CollectionsFragment() {
}
class CalendarPermission(val context: Context): LiveData<Boolean>() {
init {
check()
}
fun check() {
value = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED
}
}
}

View File

@@ -57,11 +57,11 @@ class BatteryOptimizationsFragment: Fragment() {
binding.batteryText.text = getString(R.string.intro_battery_text, getString(R.string.app_name))
binding.autostartHeading.text = getString(R.string.intro_autostart_title, WordUtils.capitalize(Build.MANUFACTURER))
binding.autostartText.text = getString(R.string.intro_autostart_text)
binding.autostartText.setText(R.string.intro_autostart_text)
binding.autostartMoreInfo.setOnClickListener {
UiUtils.launchUri(requireActivity(), App.homepageUrl(requireActivity()).buildUpon()
.appendPath("faq").appendPath("synchronization-is-not-run-as-expected")
.appendQueryParameter("manufacturer", Build.MANUFACTURER).build())
.appendQueryParameter("manufacturer", Build.MANUFACTURER.toLowerCase()).build())
}
binding.infoLeaveUnchecked.text = getString(R.string.intro_leave_unchecked, getString(R.string.app_settings_reset_hints))

View File

@@ -1,11 +1,10 @@
package at.bitfire.davdroid.ui.intro
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
@@ -19,11 +18,13 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import at.bitfire.davdroid.App
import at.bitfire.davdroid.PackageChangedReceiver
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.IntroOpentasksBinding
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.UiUtils
import at.bitfire.davdroid.ui.intro.IIntroFragmentFactory.ShowMode
import at.bitfire.davdroid.ui.intro.OpenTasksFragment.Model.Companion.HINT_OPENTASKS_NOT_INSTALLED
import com.google.android.material.snackbar.Snackbar
@@ -88,8 +89,8 @@ class OpenTasksFragment: Fragment() {
var isInstalled = MutableLiveData<Boolean>()
val shallBeInstalled = MutableLiveData<Boolean>()
val openTasksInstalledReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val tasksWatcher = object: PackageChangedReceiver(app) {
override fun onReceive(context: Context?, intent: Intent?) {
checkInstalled()
}
}
@@ -106,17 +107,11 @@ class OpenTasksFragment: Fragment() {
}
init {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
app.registerReceiver(openTasksInstalledReceiver, filter)
checkInstalled()
}
override fun onCleared() {
getApplication<Application>().unregisterReceiver(openTasksInstalledReceiver)
tasksWatcher.close()
}
fun checkInstalled() {
@@ -130,11 +125,16 @@ class OpenTasksFragment: Fragment() {
class Factory: IIntroFragmentFactory {
override fun shouldBeShown(context: Context, settings: Settings) =
if (!LocalTaskList.tasksProviderAvailable(context) && settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED) != false)
IIntroFragmentFactory.ShowMode.SHOW
else
IIntroFragmentFactory.ShowMode.DONT_SHOW
override fun shouldBeShown(context: Context, settings: Settings): ShowMode {
// On Android <6, OpenTasks must be installed before DAVx5, so this fragment is not useful.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return ShowMode.DONT_SHOW
return if (!LocalTaskList.tasksProviderAvailable(context) && settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED) != false)
ShowMode.SHOW
else
ShowMode.DONT_SHOW
}
override fun create() = OpenTasksFragment()

View File

@@ -0,0 +1,25 @@
package at.bitfire.davdroid.ui.intro
import android.content.Context
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS
import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMSSIONS
import at.bitfire.davdroid.PermissionUtils.TASKS_PERMISSIONS
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.PermissionsFragment
import at.bitfire.davdroid.ui.intro.IIntroFragmentFactory.ShowMode
class PermissionsFragmentFactory: IIntroFragmentFactory {
override fun shouldBeShown(context: Context, settings: Settings): IIntroFragmentFactory.ShowMode {
// show PermissionsFragment as intro fragment when no permissions are granted
val permissions = CONTACT_PERMSSIONS + CALENDAR_PERMISSIONS + TASKS_PERMISSIONS
return if (PermissionUtils.haveAnyPermission(context, permissions))
ShowMode.DONT_SHOW
else
ShowMode.SHOW
}
override fun create() = PermissionsFragment()
}

View File

@@ -36,8 +36,10 @@ import at.bitfire.davdroid.settings.Settings
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.concurrent.thread
class AccountDetailsFragment: Fragment() {
@@ -129,7 +131,7 @@ class AccountDetailsFragment: Fragment() {
fun createAccount(name: String, credentials: Credentials, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData<Boolean> {
val result = MutableLiveData<Boolean>()
val context = getApplication<Application>()
thread {
viewModelScope.launch(Dispatchers.Default + NonCancellable) {
val account = Account(name, context.getString(R.string.account_type))
// create Android account
@@ -139,7 +141,7 @@ class AccountDetailsFragment: Fragment() {
val accountManager = AccountManager.get(context)
if (!accountManager.addAccountExplicitly(account, credentials.password, userData)) {
result.postValue(false)
return@thread
return@launch
}
// add entries for account to service DB
@@ -163,10 +165,11 @@ class AccountDetailsFragment: Fragment() {
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
context.startService(refreshIntent)
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_address_books.xml
accountSettings.setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_SYNC_INTERVAL)
} else
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 0)
// set default sync interval and enable sync regardless of permissions
val addrBookAuthority = context.getString(R.string.address_books_authority)
ContentResolver.setIsSyncable(account, addrBookAuthority, 1)
accountSettings.setSyncInterval(addrBookAuthority, Constants.DEFAULT_SYNC_INTERVAL)
}
if (config.calDAV != null) {
// insert CalDAV service
@@ -176,24 +179,21 @@ class AccountDetailsFragment: Fragment() {
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
context.startService(refreshIntent)
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_calendars.xml
// set default sync interval and enable sync regardless of permissions
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
accountSettings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL)
// enable task sync if OpenTasks is installed
// further changes will be handled by PackageChangedReceiver
if (LocalTaskList.tasksProviderAvailable(context)) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1)
accountSettings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL)
// further changes will be handled by OpenTasksWatcher on app start or when OpenTasks is (un)installed
}
} else {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0)
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)
}
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Couldn't access account settings", e)
result.postValue(false)
return@thread
return@launch
}
result.postValue(true)
}

View File

@@ -7,7 +7,6 @@ import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger.log
/**
* [android.widget.ImageView] that supports directional cropping in both vertical and

View File

@@ -0,0 +1,346 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1144dp"
android:height="640.99dp"
android:viewportWidth="1144"
android:viewportHeight="640.99">
<path
android:pathData="m1144,372.81a180.56,168.7 0,0 1,-26.12 87.71c0,32.35 -16.14,61.67 -42.32,83.03a144.74,135.23 0,0 1,-21.08 14.28c-0.3,0.18 -0.61,0.35 -0.92,0.51 -0.56,0.31 -1.12,0.62 -1.69,0.93l-0.53,0.29a164.61,153.8 0,0 1,-81.1 19.43h-753.07q-9,0 -17.84,-0.67a211.15,197.28 0,0 1,-61.06 -13.21q-5.72,-2.11 -11.22,-4.54 -2.62,-1.15 -5.19,-2.36c-1.07,-0.5 -2.14,-1.03 -3.2,-1.55a187.42,175.11 0,0 1,-34.46 -21.85c-34,-27.31 -55.08,-65.04 -55.08,-106.71a180.5,168.64 0,0 1,-29.12 -92.19c0,-92.63 79.24,-167.72 177,-167.72 3,0 6,0.08 9,0.23 0.43,0 0.85,0 1.28,0.06 16.48,-26.25 38.51,-50.36 65,-71.59 72.24,-57.83 177.82,-94.29 295.43,-94.29 98.82,0 189.15,25.74 258.34,68.27a174.18,162.74 0,0 1,85.95 -21.07c97.74,0 177,75.09 177,167.72a184.9,172.76 0,0 1,-1 17.55,180 168.18,0 0,1 76,137.74z"
android:strokeAlpha="0.1"
android:strokeWidth=".9666"
android:fillColor="#ff9e40"
android:fillAlpha="0.1"/>
<path
android:pathData="m138.37,562.79c0,0.46 -0.05,0.91 -0.1,1.35q-5.72,-2.26 -11.22,-4.86a9.1,9.1 0,0 1,-5.19 -2.53c-1.07,-0.54 -2.14,-1.1 -3.2,-1.66a13.68,13.68 0,0 1,0.81 -1.46c2.06,-3.26 5.19,-5.3 8.64,-5.19s6.44,2.34 8.29,5.71a16.87,16.87 0,0 1,1.97 8.64z"
android:strokeAlpha="0.2"
android:fillColor="#3f3d56"
android:fillAlpha="0.2"/>
<path
android:pathData="m138.91,545.67a16.88,16.88 0,0 1,-2.51 8.48c-2.06,3.25 -5.19,5.29 -8.64,5.18l-0.71,-0.05a9.1,9.1 0,0 1,-5.19 -2.53,12.53 12.53,0 0,1 -2.39,-3.12 17.55,17.55 0,0 1,0.54 -17.12c2.06,-3.26 5.19,-5.3 8.64,-5.19s6.44,2.34 8.29,5.71a16.87,16.87 0,0 1,1.97 8.64z"
android:strokeAlpha="0.2"
android:fillColor="#3f3d56"
android:fillAlpha="0.2"/>
<path
android:pathData="M128.32,542.24a14.01,10.7 91.81,1 0,0.89 -28.01a14.01,10.7 91.81,1 0,-0.89 28.01z"
android:strokeAlpha="0.2"
android:fillColor="#3f3d56"
android:fillAlpha="0.2"/>
<path
android:pathData="M128.85,525.12a14.01,10.7 91.81,1 0,0.89 -28.01a14.01,10.7 91.81,1 0,-0.89 28.01z"
android:strokeAlpha="0.2"
android:fillColor="#3f3d56"
android:fillAlpha="0.2"/>
<path
android:pathData="M129.49,506.9a14.02,10.7 91.72,1 0,0.84 -28.02a14.02,10.7 91.72,1 0,-0.84 28.02z"
android:strokeAlpha="0.2"
android:fillColor="#3f3d56"
android:fillAlpha="0.2"/>
<path
android:pathData="m93.47,375.55a49.66,49.66 0,0 1,-3.8 -6l28.26,-3.73 -30.42,-0.73a51.38,51.38 0,0 1,0.31 -40.64l40.12,22.45 -36.75,-28.85a51.28,51.28 0,1 1,82.84 60,51.12 51.12,0 0,1 5.55,9.53l-37.09,17.81 39.33,-11.83a51.34,51.34 0,0 1,-9.82 47.91,51.28 51.28,0 1,1 -80.56,-2.54 51.28,51.28 0,0 1,2 -63.38z"
android:strokeAlpha="0.2"
android:fillColor="#ff9e40"
android:fillAlpha="0.2"/>
<path
android:fillColor="#FF000000"
android:pathData="m184,410.13a51.06,51.06 0,0 1,-12 31.34,51.28 51.28,0 1,1 -80.56,-2.54c-6.57,-8.93 92.74,-34.56 92.56,-28.8z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="m958,81.41v479.18a18.41,18.41 0,0 1,-18.41 18.41h-735.18a18.13,18.13 0,0 1,-5.08 -0.72,18.38 18.38,0 0,1 -13.33,-17.69v-213.84,-3.25 -203.94c0.43,0 0.85,0 1.28,0.06 16.48,-28.09 38.51,-53.9 65,-76.62h687.31a18.41,18.41 0,0 1,18.41 18.41z"
android:fillColor="#fff"/>
<path
android:pathData="M730.15,125.23L832.02,125.23A17.98,17.98 0,0 1,850 143.21L850,143.21A17.98,17.98 0,0 1,832.02 161.19L730.15,161.19A17.98,17.98 0,0 1,712.17 143.21L712.17,143.21A17.98,17.98 0,0 1,730.15 125.23z"
android:fillColor="#3f3d56"/>
<path
android:fillColor="#FF000000"
android:pathData="M733.12,131.24L829.04,131.24A11.97,11.97 0,0 1,841.01 143.21L841.01,143.21A11.97,11.97 0,0 1,829.04 155.17L733.12,155.17A11.97,11.97 0,0 1,721.15 143.21L721.15,143.21A11.97,11.97 0,0 1,733.12 131.24z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="M805.38,132.55L827.36,132.55A10.65,10.65 0,0 1,838.01 143.2L838.01,143.21A10.65,10.65 0,0 1,827.36 153.86L805.38,153.86A10.65,10.65 0,0 1,794.73 143.21L794.73,143.2A10.65,10.65 0,0 1,805.38 132.55z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M816.04,143.21m-6.66,0a6.66,6.66 0,1 1,13.32 0a6.66,6.66 0,1 1,-13.32 0"
android:fillColor="#fff"/>
<path
android:pathData="M730.15,362.28L832.02,362.28A17.98,17.98 0,0 1,850 380.26L850,380.26A17.98,17.98 0,0 1,832.02 398.24L730.15,398.24A17.98,17.98 0,0 1,712.17 380.26L712.17,380.26A17.98,17.98 0,0 1,730.15 362.28z"
android:fillColor="#3f3d56"/>
<path
android:fillColor="#FF000000"
android:pathData="M733.12,368.29L829.04,368.29A11.97,11.97 0,0 1,841.01 380.26L841.01,380.26A11.97,11.97 0,0 1,829.04 392.22L733.12,392.22A11.97,11.97 0,0 1,721.15 380.26L721.15,380.26A11.97,11.97 0,0 1,733.12 368.29z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="M805.38,369.6L827.36,369.6A10.65,10.65 0,0 1,838.01 380.25L838.01,380.26A10.65,10.65 0,0 1,827.36 390.91L805.38,390.91A10.65,10.65 0,0 1,794.73 380.26L794.73,380.25A10.65,10.65 0,0 1,805.38 369.6z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M816.04,380.26m-6.66,0a6.66,6.66 0,1 1,13.32 0a6.66,6.66 0,1 1,-13.32 0"
android:fillColor="#fff"/>
<path
android:pathData="M730.15,480.8L832.02,480.8A17.98,17.98 0,0 1,850 498.78L850,498.78A17.98,17.98 0,0 1,832.02 516.76L730.15,516.76A17.98,17.98 0,0 1,712.17 498.78L712.17,498.78A17.98,17.98 0,0 1,730.15 480.8z"
android:fillColor="#3f3d56"/>
<path
android:fillColor="#FF000000"
android:pathData="M733.12,486.82L829.04,486.82A11.97,11.97 0,0 1,841.01 498.79L841.01,498.79A11.97,11.97 0,0 1,829.04 510.75L733.12,510.75A11.97,11.97 0,0 1,721.15 498.79L721.15,498.79A11.97,11.97 0,0 1,733.12 486.82z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="M805.38,488.13L827.36,488.13A10.65,10.65 0,0 1,838.01 498.78L838.01,498.79A10.65,10.65 0,0 1,827.36 509.44L805.38,509.44A10.65,10.65 0,0 1,794.73 498.79L794.73,498.78A10.65,10.65 0,0 1,805.38 488.13z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M816.04,498.78m-6.66,0a6.66,6.66 0,1 1,13.32 0a6.66,6.66 0,1 1,-13.32 0"
android:fillColor="#fff"/>
<path
android:pathData="M304.65,102.59L363.92,102.59A10.65,10.65 0,0 1,374.57 113.24L374.57,113.25A10.65,10.65 0,0 1,363.92 123.9L304.65,123.9A10.65,10.65 0,0 1,294 113.25L294,113.24A10.65,10.65 0,0 1,304.65 102.59z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,132.55L507.08,132.55A10.65,10.65 0,0 1,517.73 143.2L517.73,143.21A10.65,10.65 0,0 1,507.08 153.86L304.65,153.86A10.65,10.65 0,0 1,294 143.21L294,143.2A10.65,10.65 0,0 1,304.65 132.55z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,162.52L507.08,162.52A10.65,10.65 0,0 1,517.73 173.17L517.73,173.18A10.65,10.65 0,0 1,507.08 183.83L304.65,183.83A10.65,10.65 0,0 1,294 173.18L294,173.17A10.65,10.65 0,0 1,304.65 162.52z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,221.11L363.92,221.11A10.65,10.65 0,0 1,374.57 231.76L374.57,231.77A10.65,10.65 0,0 1,363.92 242.42L304.65,242.42A10.65,10.65 0,0 1,294 231.77L294,231.76A10.65,10.65 0,0 1,304.65 221.11z"
android:strokeAlpha="0.5"
android:fillColor="#ff9e40"
android:fillAlpha="0.5"/>
<path
android:pathData="M304.65,251.08L450.35,251.08A10.65,10.65 0,0 1,461 261.73L461,261.74A10.65,10.65 0,0 1,450.35 272.39L304.65,272.39A10.65,10.65 0,0 1,294 261.74L294,261.73A10.65,10.65 0,0 1,304.65 251.08z"
android:strokeAlpha="0.5"
android:fillColor="#ff9e40"
android:fillAlpha="0.5"/>
<path
android:pathData="M304.65,281.04L507.08,281.04A10.65,10.65 0,0 1,517.73 291.69L517.73,291.7A10.65,10.65 0,0 1,507.08 302.35L304.65,302.35A10.65,10.65 0,0 1,294 291.7L294,291.69A10.65,10.65 0,0 1,304.65 281.04z"
android:strokeAlpha="0.5"
android:fillColor="#ff9e40"
android:fillAlpha="0.5"/>
<path
android:pathData="M304.65,339.64L363.92,339.64A10.65,10.65 0,0 1,374.57 350.29L374.57,350.3A10.65,10.65 0,0 1,363.92 360.95L304.65,360.95A10.65,10.65 0,0 1,294 350.3L294,350.29A10.65,10.65 0,0 1,304.65 339.64z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,369.6L507.08,369.6A10.65,10.65 0,0 1,517.73 380.25L517.73,380.26A10.65,10.65 0,0 1,507.08 390.91L304.65,390.91A10.65,10.65 0,0 1,294 380.26L294,380.25A10.65,10.65 0,0 1,304.65 369.6z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,399.57L422.35,399.57A10.65,10.65 0,0 1,433 410.22L433,410.23A10.65,10.65 0,0 1,422.35 420.88L304.65,420.88A10.65,10.65 0,0 1,294 410.23L294,410.22A10.65,10.65 0,0 1,304.65 399.57z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,458.16L363.92,458.16A10.65,10.65 0,0 1,374.57 468.81L374.57,468.82A10.65,10.65 0,0 1,363.92 479.47L304.65,479.47A10.65,10.65 0,0 1,294 468.82L294,468.81A10.65,10.65 0,0 1,304.65 458.16z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,488.13L507.08,488.13A10.65,10.65 0,0 1,517.73 498.78L517.73,498.79A10.65,10.65 0,0 1,507.08 509.44L304.65,509.44A10.65,10.65 0,0 1,294 498.79L294,498.78A10.65,10.65 0,0 1,304.65 488.13z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M304.65,518.09L463.35,518.09A10.65,10.65 0,0 1,474 528.74L474,528.75A10.65,10.65 0,0 1,463.35 539.4L304.65,539.4A10.65,10.65 0,0 1,294 528.75L294,528.74A10.65,10.65 0,0 1,304.65 518.09z"
android:fillColor="#ff9e40"/>
<path
android:strokeWidth="1"
android:pathData="M260,202.99L912,202.99"
android:strokeAlpha="0.7"
android:fillColor="#00000000"
android:strokeColor="#3f3d56"
android:fillAlpha="0.7"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M260,323.49L912,323.49"
android:strokeAlpha="0.7"
android:fillColor="#00000000"
android:strokeColor="#3f3d56"
android:fillAlpha="0.7"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M260,443.99L912,443.99"
android:strokeAlpha="0.7"
android:fillColor="#00000000"
android:strokeColor="#3f3d56"
android:fillAlpha="0.7"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M260,567.49L912,567.49"
android:strokeAlpha="0.7"
android:fillColor="#00000000"
android:strokeColor="#3f3d56"
android:fillAlpha="0.7"
android:strokeLineCap="round"/>
<path
android:pathData="M647.32,630.49a147.68,10.5 0,1 0,295.36 0a147.68,10.5 0,1 0,-295.36 0z"
android:strokeAlpha="0.1"
android:fillColor="#ff9e40"
android:fillAlpha="0.1"/>
<path
android:pathData="M48.89,116.68a21.63,20.21 0,1 0,43.26 0a21.63,20.21 0,1 0,-43.26 0z"
android:strokeAlpha="0.1"
android:strokeWidth=".9666"
android:fillColor="#ff9e40"
android:fillAlpha="0.1"/>
<path
android:pathData="M59.04,44.98a21.63,20.21 0,1 0,43.26 0a21.63,20.21 0,1 0,-43.26 0z"
android:strokeAlpha="0.1"
android:strokeWidth=".9666"
android:fillColor="#ff9e40"
android:fillAlpha="0.1"/>
<path
android:pathData="M117.09,81.72a36.25,33.87 0,1 0,72.5 0a36.25,33.87 0,1 0,-72.5 0z"
android:strokeAlpha="0.1"
android:strokeWidth=".9666"
android:fillColor="#ff9e40"
android:fillAlpha="0.1"/>
<path
android:pathData="M730.15,243.75L832.02,243.75A17.98,17.98 0,0 1,850 261.73L850,261.73A17.98,17.98 0,0 1,832.02 279.71L730.15,279.71A17.98,17.98 0,0 1,712.17 261.73L712.17,261.73A17.98,17.98 0,0 1,730.15 243.75z"
android:fillColor="#3f3d56"/>
<path
android:fillColor="#FF000000"
android:pathData="M733.12,249.77L829.04,249.77A11.97,11.97 0,0 1,841.01 261.74L841.01,261.74A11.97,11.97 0,0 1,829.04 273.7L733.12,273.7A11.97,11.97 0,0 1,721.15 261.74L721.15,261.74A11.97,11.97 0,0 1,733.12 249.77z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="m771.68,257.4c-0.49,-2.31 -0.85,-4.71 -2.12,-6.69a15.16,15.16 0,0 0,-3.12 -3.28,20.74 20.74,0 0,0 -6.68,-4.17 8.34,8.34 0,0 0,-7.56 0.87c-2.11,1.6 -3,4.34 -3.33,7a17.07,17.07 0,0 0,1.78 10.55c2.05,3.56 5.59,5.94 8.56,8.78a34.39,34.39 0,0 1,5.58 7,20.21 20.21,0 0,0 2.54,3.72 4.84,4.84 0,0 0,4 1.61,7 7,0 0,0 3.27,-1.93c1.22,-1.07 4.5,-3 4.6,-4.67 0.08,-1.37 -2.48,-4.47 -3.11,-5.76a65.41,65.41 0,0 1,-4.41 -13.03z"
android:fillColor="#fbbebe"/>
<path
android:fillColor="#FF000000"
android:pathData="m771.68,257.4c-0.49,-2.31 -0.85,-4.71 -2.12,-6.69a15.16,15.16 0,0 0,-3.12 -3.28,20.74 20.74,0 0,0 -6.68,-4.17 8.34,8.34 0,0 0,-7.56 0.87c-2.11,1.6 -3,4.34 -3.33,7a17.07,17.07 0,0 0,1.78 10.55c2.05,3.56 5.59,5.94 8.56,8.78a34.39,34.39 0,0 1,5.58 7,20.21 20.21,0 0,0 2.54,3.72 4.84,4.84 0,0 0,4 1.61,7 7,0 0,0 3.27,-1.93c1.22,-1.07 4.5,-3 4.6,-4.67 0.08,-1.37 -2.48,-4.47 -3.11,-5.76a65.41,65.41 0,0 1,-4.41 -13.03z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="m840.85,323.23a58.88,58.88 0,0 0,-16.18 -2c-9.11,0.13 -18.49,2.34 -27.17,-0.46a47.4,47.4 0,0 1,-6.5 -2.77,16.58 16.58,0 0,1 -4.86,-3.19c-4.32,-4.57 -6.18,-11.46 -7.52,-17.6a117.44,117.44 0,0 1,-2.05 -18.73c-2.25,1.05 -4.63,2.08 -7.1,1.87s-5,-2.11 -5,-4.59c0.37,5.82 0.64,11.71 -0.33,17.45 -0.38,2.28 -1,4.53 -1.17,6.83 -0.74,8.33 3.63,16.65 2.07,24.86a6,6 0,0 0,2.83 6.42c2.24,1.21 4.87,1.36 7.33,2 4.83,1.23 9,4.3 13.9,5.45 2.25,0.53 4.6,0.63 6.84,1.23 3.19,0.85 6.1,2.68 9.36,3.25 2.44,0.44 5,0.14 7.43,0.35 2.15,0.18 4.25,0.74 6.41,0.8 5.28,0.13 10.26,-2.85 13.76,-6.81s5.73,-9.59 7.95,-14.36z"
android:fillColor="#f86d70"/>
<path
android:pathData="m840.85,323.23a58.88,58.88 0,0 0,-16.18 -2c-9.11,0.13 -18.49,2.34 -27.17,-0.46a47.4,47.4 0,0 1,-6.5 -2.77,16.58 16.58,0 0,1 -4.86,-3.19c-4.32,-4.57 -6.18,-11.46 -7.52,-17.6a117.44,117.44 0,0 1,-2.05 -18.73c-2.25,1.05 -4.63,2.08 -7.1,1.87s-5,-2.11 -5,-4.59c0.37,5.82 0.64,11.71 -0.33,17.45 -0.38,2.28 -1,4.53 -1.17,6.83 -0.74,8.33 3.63,16.65 2.07,24.86a6,6 0,0 0,2.83 6.42c2.24,1.21 4.87,1.36 7.33,2 4.83,1.23 9,4.3 13.9,5.45 2.25,0.53 4.6,0.63 6.84,1.23 3.19,0.85 6.1,2.68 9.36,3.25 2.44,0.44 5,0.14 7.43,0.35 2.15,0.18 4.25,0.74 6.41,0.8 5.28,0.13 10.26,-2.85 13.76,-6.81s5.73,-9.59 7.95,-14.36z"
android:strokeAlpha="0.749"
android:fillColor="#6569f7"
android:fillAlpha="0.749"/>
<path
android:pathData="m855.31,580.67a13.69,13.69 0,0 1,4.67 1.46,11 11,0 0,1 4.06,5.48c2.18,5.38 2.63,11.3 2.73,17.11 0.08,4.69 -0.08,9.49 -1.76,13.87a8.83,8.83 0,0 1,-2.31 3.63,8.33 8.33,0 0,1 -3.18,1.54c-2.51,0.69 -5.41,0.68 -7.46,-0.92 -2.28,-1.78 -2.85,-4.93 -3.33,-7.78 -0.65,-3.84 -1.57,-7.93 -4.43,-10.58 -2.12,-2 -5.16,-3 -6.7,-5.42 -1.84,-2.91 -0.87,-6.71 0.3,-9.95 1,-2.81 2.51,-8.5 5.45,-9.94s8.91,0.95 11.96,1.5z"
android:fillColor="#fbbebe"/>
<path
android:pathData="m747,594.47a22.15,22.15 0,0 0,4.58 8.51c1.3,1.5 2.93,3.32 2.25,5.18a4.82,4.82 0,0 1,-1.23 1.65c-9.66,9.44 -23.17,14 -36.57,15.72a1.29,1.29 0,0 1,-0.65 0c-0.39,-0.17 -0.49,-0.67 -0.53,-1.1a13.51,13.51 0,0 1,0 -4.21c0.65,-3.27 3.52,-5.58 6.33,-7.39s5.88,-3.55 7.44,-6.5c1.95,-3.66 1.13,-8.43 3.45,-11.86 1.88,-2.79 5.38,-4 8.71,-4.5 1.15,-0.18 3.6,-1.44 4.58,-0.86s1.3,4.21 1.64,5.36z"
android:fillColor="#fbbebe"/>
<path
android:pathData="m856.63,580.33c-0,-0.67 0.5,-1.23 1.16,-1.32 0.65,-0.06 1.3,0.02 1.91,0.24 1.5,0.42 3.04,0.7 4.59,0.85 1.86,0.06 3.71,0.28 5.54,0.65 1.85,0.39 3.43,1.59 4.29,3.27 0.36,0.99 0.52,2.05 0.47,3.1 0.04,4.04 -0.44,8.08 -1.43,12 -0.08,0.3 0.15,0.76 -0.14,0.87s-0.17,0.52 -0.4,0.63c-0.04,0.94 -0.81,1.45 -0.88,1.68 -0.97,2.56 -1.76,3.49 -1.9,6.23 -0.44,4 -1,8.06 -1.67,12.06 -0.39,2.34 -0.88,4.8 -2.42,6.6 -1.69,2 -4.36,2.83 -6.94,3.18 -5.44,0.72 -10.93,-0.37 -16.3,-1.49 -1.03,-0.1 -1.99,-0.53 -2.75,-1.23 -0.49,-0.61 -0.73,-1.39 -0.67,-2.18 0.12,-2.69 2.6,-4.59 4.83,-6.09l3.09,-2.07c0.22,-0.12 0.4,-0.29 0.54,-0.49 0.15,-0.31 0.2,-0.66 0.15,-1l-0.2,-4c4.12,-0.72 5.93,-0.28 7.99,-2.52 3.01,-4.06 4.15,-19.59 3.71,-23.65 -0.07,-1.1 -0.41,-2.17 -1,-3.1 -0.47,-0.67 -1.46,-1.32 -1.57,-2.22z"
android:fillColor="#333"/>
<path
android:pathData="M756.78,272.39L734.8,272.39A10.65,10.65 0,0 1,724.15 261.74L724.15,261.73A10.65,10.65 0,0 1,734.8 251.08L756.78,251.08A10.65,10.65 0,0 1,767.43 261.73L767.43,261.74A10.65,10.65 0,0 1,756.78 272.39z"
android:fillColor="#ff9e40"/>
<path
android:pathData="M746.12,261.73m-6.66,0a6.66,6.66 0,1 1,13.32 0a6.66,6.66 0,1 1,-13.32 0"
android:fillColor="#fff"/>
<path
android:pathData="m726.88,616.51c12.16,-1.99 19.92,-4.57 21.42,-5.88 1.21,-1.02 2.28,-2.19 3.19,-3.49 0.85,-1.29 1.57,-2.87 3,-3.41 0.87,3.76 1.32,7.6 1.34,11.45 0,2.54 -0.36,5.43 -2.43,6.9 -0.83,0.52 -1.74,0.89 -2.7,1.1l-4.52,1.28c-0.8,0.3 -1.67,0.38 -2.51,0.24 -0.53,-0.14 -0.86,0.15 -1.41,0.05 -0.6,-0.06 -1.23,0.04 -1.79,0.26l-3.91,1.14c-1.32,0.34 -2.61,0.79 -3.86,1.35 -1.12,0.53 -2.26,0.56 -3.35,1.15 -1.82,0.91 -3.72,1.68 -5.66,2.3l-6.87,2.39c-1.88,0.74 -3.85,1.24 -5.85,1.5 -1.38,0.12 -2.79,0 -4.16,0.23 -1,0.18 -2,0.56 -3.1,0.64 -1.74,0.05 -3.44,-0.45 -4.87,-1.44 -0.56,-0.31 -1.04,-0.74 -1.42,-1.26 -1,-1.53 -0.13,-3.55 0.83,-5.1 1.69,-2.94 4.1,-5.41 7,-7.18 1.2,-0.66 2.49,-1.14 3.72,-1.74 3.51,-1.7 6.16,-4.54 9.47,-6.48 3.13,-1.83 3.45,2.75 4.21,4.01z"
android:fillColor="#333"/>
<path
android:pathData="m818.69,340.94a28.57,28.57 0,0 0,12.16 4.73,49.5 49.5,0 0,0 6.88,0.37 174.85,174.85 0,0 0,28.63 -2.39c-3.76,-4.15 -7.57,-8.59 -9,-14a23.11,23.11 0,0 1,-0.44 -8.77,66 66,0 0,1 2.68,-11 77.63,77.63 0,0 1,-12.1 -0.31c-2.29,-0.24 -7.91,-2.32 -9.9,-0.74s-0.46,6.67 -0.43,9.06a2.26,2.26 0,0 1,0 0.26,28.93 28.93,0 0,1 -2.31,10.58c-2.86,6.46 -9.06,11.78 -16.17,12.21z"
android:fillColor="#fbbebe"/>
<path
android:fillColor="#FF000000"
android:pathData="m837.18,317.84a24.06,24.06 0,0 0,19.75 3,66 66,0 0,1 2.68,-11 77.63,77.63 0,0 1,-12.1 -0.31c-2.29,-0.24 -7.91,-2.32 -9.9,-0.74s-0.46,6.66 -0.43,9.05z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="M850.38,296.54m-24.11,0a24.11,24.11 0,1 1,48.22 0a24.11,24.11 0,1 1,-48.22 0"
android:fillColor="#fbbebe"/>
<path
android:pathData="m748.09,418.53c-0.02,3.96 2.05,7.63 5.44,9.67 1.86,0.97 3.84,1.68 5.9,2.08 11,2.66 22.32,4.8 33.65,4.15 2.46,-0.02 4.88,-0.53 7.14,-1.5 2.28,-1.01 3.92,-3.07 4.41,-5.51 0.01,-0.6 0.13,-1.19 0.34,-1.75 0.44,-0.61 1.01,-1.12 1.67,-1.49 2.1,-1.58 3.11,-4.24 2.59,-6.82 1.37,0.62 2.81,-0.77 3.46,-2.13s1.15,-3 2.53,-3.59c0.94,-0.4 2,-0.16 3,-0.42 1.59,-0.41 2.56,-2 3.68,-3.15 2.28,-2.44 5.53,-3.7 8.51,-5.21 8.88,-4.47 16.11,-11.56 23.2,-18.53 3.69,-3.4 7.03,-7.14 10,-11.18 0.79,-1.16 1.53,-2.36 2.2,-3.6 2.14,-4.11 3.99,-8.35 5.55,-12.71 0.9,-2.34 1.81,-4.8 1.44,-7.28 0,-0.22 -0.08,-0.44 -0.13,-0.67 -0.46,-1.59 -1.2,-3.09 -2.19,-4.42 -2.45,-3.6 -5.66,-7 -9.88,-8.12s-8.42,0.23 -12.71,1.14c-1.42,0.32 -2.86,0.54 -4.31,0.66 -4.54,0.32 -9,-0.9 -13.42,-2.11l-4.78,-1.32c-2.39,-0.66 -3.59,-3.55 -6,-4.06 -1.08,-0.16 -2.19,-0.11 -3.25,0.17 -4.91,0.93 -4.33,3.22 -8.66,5.72 -0.97,0.55 -2.28,2.11 -3.14,2.8 0.7,3.12 -2.25,5.07 -3.27,8.85 -1,4.17 -4.33,8.98 -7.64,12.53 -1.7,2.79 -4.62,5.32 -6.64,8.22l-13.01,13.79c-2.63,3.95 -5.21,7.11 -8.35,10.66 -6.34,6.89 -17.16,15.76 -17.33,25.11z"
android:fillColor="#b36df8"/>
<path
android:pathData="m779.53,257.4c-0.49,-2.31 -0.85,-4.71 -2.12,-6.69a15.16,15.16 0,0 0,-3.12 -3.28,20.74 20.74,0 0,0 -6.68,-4.17 8.34,8.34 0,0 0,-7.56 0.87c-2.11,1.6 -3,4.34 -3.33,7a17.07,17.07 0,0 0,1.78 10.55c2,3.56 5.59,5.94 8.56,8.78a34.39,34.39 0,0 1,5.58 7,20.21 20.21,0 0,0 2.54,3.72 4.84,4.84 0,0 0,4 1.61,7 7,0 0,0 3.27,-1.93c1.22,-1.07 4.5,-3 4.6,-4.67 0.08,-1.37 -2.48,-4.47 -3.11,-5.76a65.41,65.41 0,0 1,-4.41 -13.03z"
android:fillColor="#fbbebe"/>
<path
android:pathData="m694.81,536.64c3,5.6 6.34,11 9.05,16.8 1.57,3.37 2.93,6.83 4.68,10.11 1.91,3.57 4.26,6.89 6.26,10.41 5,8.9 7.83,18.94 10.42,28.83a57.39,57.39 0,0 0,20.52 -7.22,12.9 12.9,0 0,0 1.23,-0.79 14.52,14.52 0,0 0,4.14 -4.57c-2.39,-9 -4.84,-18.23 -9.71,-26.22 -1.59,-2.6 -3.41,-5 -5,-7.61 -4.73,-7.45 -5.71,-16.84 -11.12,-23.81a4.08,4.08 0,0 1,0 -4.84c0.62,-0.81 1.62,-1.58 1.43,-2.59 -0.14,-0.73 -0.9,-1.39 -0.61,-2.07 0.14,-0.33 0.48,-0.51 0.74,-0.75 0.92,-0.84 0.8,-2.29 0.89,-3.53 0.2,-3.16 2.09,-5.93 3.92,-8.51a5.34,5.34 0,0 1,1.44 -1.55c0.49,-0.3 1.07,-0.44 1.56,-0.74 1.72,-1.07 1.6,-3.59 2.43,-5.46 1.08,-2.46 3.8,-3.71 5.59,-5.7 2.27,-2.54 2.84,-6.07 5.24,-8.29a7.11,7.11 0,0 1,0.79 -0.64,39.7 39.7,0 0,0 -1.41,11.61c-0.23,13.44 3.13,27.34 1.56,40.69 -1.24,10.58 5.28,21.14 14.52,26.44a70.28,70.28 0,0 0,11.31 4.76c5.78,2.05 11.56,4.16 17.23,6.53 4,1.66 8,3.46 11.86,5.37a146,146 0,0 1,14 7.76c5.57,3.54 11.15,7.14 15.1,12.43 3.83,-0.1 7.34,-2.4 9.76,-5.37s3.93,-6.56 5.34,-10.11c1.23,-3.07 2.45,-6.22 2.54,-9.53a1.9,1.9 0,0 0,-1.06 -1.71l-26.46,-14 -5,-2.62c-10.83,-5.75 -22,-11.07 -32.89,-16.81 -0.73,0.21 -1.33,-0.62 -1.54,-1.34s-0.36,-1.62 -1.05,-1.94c-0.35,-0.16 -0.79,-0.16 -1,-0.44a1.2,1.2 0,0 1,-0.18 -0.88c0.35,-5.24 4.13,-10.06 3.37,-15.25 -0.32,-2.22 -1.47,-4.4 -1,-6.57 0.35,-1.51 1.43,-2.73 2,-4.16 1.29,-3.2 0,-7 1.42,-10.17a23.34,23.34 0,0 0,1.91 -6.48,43.88 43.88,0 0,1 4.05,-12.84 23.18,23.18 0,0 1,3.72 -5.64c0.8,-0.85 1.69,-1.62 2.44,-2.5 2.31,-2.73 3.15,-6.37 3.81,-9.88 1.18,-6.37 2,-13 0.52,-19.29 -0.5,-2.15 -1.26,-4.28 -1.23,-6.49 0,-2.91 1.31,-6.32 -0.71,-8.43a6.6,6.6 0,0 0,-3 -1.52,90.43 90.43,0 0,0 -14.35,-2.65l-22.06,-2.87a20.28,20.28 0,0 1,-7.35 -1.91c-3,-1.65 -5,-4.72 -6.2,-7.92 -3.88,2.83 -5.34,8.1 -8.43,11.76 -1.51,1.79 -3.37,3.23 -5,4.87a55.11,55.11 0,0 0,-5.54 6.83c-4.61,6.33 -9.25,12.71 -12.55,19.8 -1.71,3.69 -3,7.54 -4.42,11.36 -5.39,15.07 -14.18,29.37 -17.83,44.94 -0.9,3.81 -3.23,7.24 -3.58,11.14 -0.41,4.13 1.5,8.13 3.49,11.77z"
android:fillColor="#434175"/>
<path
android:fillColor="#FF000000"
android:pathData="m810.53,411.54a6.85,6.85 0,0 1,0.81 3.26c-0.49,-2.44 -3.85,-3.79 -4.06,-6.2 -0.1,-1.11 0.1,-1.06 0.79,-0.47 1.05,0.87 1.5,2.45 2.46,3.41z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:fillColor="#FF000000"
android:pathData="m800.32,418.94a5.15,5.15 0,0 1,1.53 2.26,5.75 5.75,0 0,1 0.3,3.93c-0.49,-0.23 -0.66,-0.83 -0.81,-1.36a9.38,9.38 0,0 0,-1 -2.33c-0.4,-0.63 -2.23,-2.13 -2.21,-2.82 -0.03,-1.04 1.7,-0.05 2.19,0.32z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:fillColor="#FF000000"
android:pathData="m804.5,340.76c1.55,1.33 -2.1,-0.95 -0.28,-0.03 4.78,2.42 10.07,3.69 14.88,6 2.37,1.21 4.68,2.54 6.9,4 4.77,3 9.53,6 14.29,9.05 1.15,0.66 2.2,1.46 3.15,2.39 1.72,1.84 1.71,5 3.28,7 1.75,2 4.27,3.16 6.93,3.2 2.63,0.02 5.25,-0.42 7.73,-1.3 1.5,-0.45 2.98,-0.98 4.42,-1.61 1.5,-0.63 0.62,-1.91 1.93,-2.88 2.65,-1.86 6.75,-4.24 7.31,-7.43 0.17,-2.18 -0.12,-4.38 -0.85,-6.44 -0.4,-1.34 -0.91,-2.65 -1.52,-3.91 -0.35,-0.71 -0.76,-1.4 -1.22,-2.05 -3.28,-4.65 -9.06,-6.84 -14.65,-7.93 -2.95,-0.57 -5.95,-0.92 -8.91,-1.42 -1.42,0.32 -2.86,0.54 -4.31,0.66 -4.54,0.32 -9,-0.9 -13.42,-2.11l-4.78,-1.32c-2.39,-0.66 -3.59,-3.55 -6,-4.06 -1.08,-0.16 -2.19,-0.11 -3.25,0.17 -4.91,0.93 -5.25,3.52 -9.58,6.02 -0.98,0.58 -2.23,2.07 -2.05,4z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="m787.49,273.36c0.61,5.83 5.53,10.11 8.53,15.14 1.46,2.45 2.48,5.13 3.79,7.66a75,75 0,0 0,5 8l5.35,7.77c3.15,4.57 6.33,9.18 10.35,13 7,1.19 12.9,6.4 19.55,8.93 5.33,2 11.13,2.3 16.72,3.39s11.37,3.27 14.65,7.93a20.83,20.83 0,0 1,2.74 6,15.54 15.54,0 0,1 0.85,6.43 12.19,12.19 0,0 1,-5 7.9,27.71 27.71,0 0,1 -8.65,4 22.74,22.74 0,0 1,-7.73 1.3,9.44 9.44,0 0,1 -6.94,-3.2c-1.56,-2 -1.55,-5.19 -3.27,-7a15.75,15.75 0,0 0,-3.15 -2.39l-14.29,-9a72.17,72.17 0,0 0,-6.9 -4c-4.81,-2.35 -10.1,-3.62 -14.88,-6s-9.23,-6.41 -10.17,-11.68c-2.91,-16.19 -14.12,-29.84 -23.7,-43.21 0.31,-0.06 0.22,-0.78 0.33,-1.08a14,14 0,0 1,7 -7.08c3.08,-1.67 6.5,-2.1 9.82,-2.81z"
android:fillColor="#b36df8"/>
<path
android:pathData="m838,273.48c-0.18,-1.76 0.17,-3.52 1,-5.08 1.59,-2.64 4.91,-3.57 7.93,-4.19s5.8,-1.1 8.72,-1.52c2.38,-0.48 4.82,-0.52 7.21,-0.13 3.26,0.72 6,2.91 9,4.26 3.6,1.61 7.6,2 11.44,2.92s7.79,2.39 10.2,5.52c1.83,2.37 2.56,5.4 3.11,8.35 4.57,24.63 -13.75,45.5 -19.98,47.75 -3.96,4.38 -1.19,2.87 -16.6,6.26 -3.32,-2.83 -7.1,-5.3 -9.83,-8.75 -4.85,-6.15 -5.78,-14.54 -5.18,-22.35 0.36,-4.64 1.2,-9.29 0.79,-13.93 -0.21,-3.55 -1.49,-6.95 -3.67,-9.76 -2.55,-3.08 -3.9,-5 -4.14,-9.35z"
android:fillColor="#434175"/>
<path
android:fillColor="#FF000000"
android:pathData="m839,268.4c1.56,-2.6 4.8,-3.54 7.77,-4.15 -1.58,0.55 -2.93,1.61 -3.85,3 -0.83,1.56 -1.17,3.33 -1,5.09 0.22,4.35 1.57,6.27 4.15,9.35 2.19,2.8 3.48,6.21 3.68,9.76 0.4,4.64 -0.43,9.28 -0.79,13.93 -0.61,7.8 0.32,16.2 5.18,22.35 2.72,3.45 6.47,5.91 9.83,8.75 -1.33,0.42 -2.42,1.09 -3.93,1.12 -3.32,-2.83 -7.1,-5.3 -9.83,-8.75 -4.85,-6.15 -5.78,-14.54 -5.18,-22.35 0.36,-4.64 1.2,-9.29 0.79,-13.93 -0.21,-3.55 -1.49,-6.95 -3.67,-9.76 -2.58,-3.08 -3.93,-5 -4.16,-9.35 -0.17,-1.75 0.18,-3.51 1.01,-5.06z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:fillColor="#FF000000"
android:pathData="m717.22,522.23 l29.78,72.55a14.52,14.52 0,0 0,4.14 -4.57c-2.39,-9 -4.84,-18.23 -9.71,-26.22 -1.59,-2.6 -3.41,-5 -5,-7.61 -4.73,-7.45 -5.71,-16.84 -11.12,-23.81a4.08,4.08 0,0 1,0 -4.84c0.62,-0.81 1.62,-1.58 1.43,-2.59 -0.14,-0.73 -0.9,-1.39 -0.61,-2.07 0.14,-0.33 0.48,-0.51 0.74,-0.75 0.92,-0.84 0.8,-2.29 0.89,-3.53 0.2,-3.16 2.09,-5.93 3.92,-8.51a5.34,5.34 0,0 1,1.44 -1.55c0.49,-0.3 1.07,-0.44 1.56,-0.74 1.72,-1.07 1.6,-3.59 2.43,-5.46 1.08,-2.46 3.8,-3.71 5.59,-5.7 2.27,-2.54 2.84,-6.07 5.24,-8.29l0.21,-0.52 -2.25,-14z"
android:strokeAlpha="0.1"
android:fillAlpha="0.1"/>
<path
android:pathData="M943.1,602.35a33.95,6.53 0,1 0,67.9 0a33.95,6.53 0,1 0,-67.9 0z"
android:fillColor="#338000"/>
<path
android:pathData="M973.67,599.57a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,593.25a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,586.92a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,580.6a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,574.27a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,567.95a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M973.67,561.63a3.95,5.17 0,1 0,7.9 0a3.95,5.17 0,1 0,-7.9 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="m962.82,518.35a18.78,18.78 0,0 1,-1.47 -2.17l10.38,-1.71 -11.23,0.09a19,19 0,0 1,-0.36 -15l15.07,7.81 -13.9,-10.21a18.94,18.94 0,1 1,31.27 21.19,18.34 18.34,0 0,1 2.16,3.45l-13.48,7 14.38,-4.82a19.07,19.07 0,0 1,1 6.07,18.85 18.85,0 0,1 -4.06,11.71 18.93,18.93 0,1 1,-29.76 0,18.94 18.94,0 0,1 0,-23.41z"
android:fillColor="#abc837"/>
<path
android:pathData="m996.64,530.05a18.85,18.85 0,0 1,-4.06 11.71,18.93 18.93,0 1,1 -29.76,0c-2.54,-3.22 33.82,-13.83 33.82,-11.71z"
android:strokeAlpha="0.1"
android:fillColor="#55d400"
android:fillAlpha="0.1"/>
<path
android:pathData="M1020.37,529.83a22.83,4.39 0,1 0,45.66 0a22.83,4.39 0,1 0,-45.66 0z"
android:fillColor="#338000"/>
<path
android:pathData="M1040.94,527.97a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,523.72a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,519.46a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,515.21a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,510.96a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,506.71a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M1040.94,502.45a2.66,3.48 0,1 0,5.32 0a2.66,3.48 0,1 0,-5.32 0z"
android:fillColor="#3f3d56"/>
<path
android:pathData="m1033.6,473.36a11,11 0,0 1,-1 -1.46l7,-1.15 -7.55,0.06a12.62,12.62 0,0 1,-1.17 -5.32,12.78 12.78,0 0,1 0.92,-4.77l10.14,5.26 -9.35,-6.87a12.73,12.73 0,1 1,21 14.25,12.56 12.56,0 0,1 1.45,2.32l-9.06,4.71 9.66,-3.25a12.77,12.77 0,0 1,-2.05 12,12.74 12.74,0 1,1 -20,0 12.71,12.71 0,0 1,0 -15.74z"
android:fillColor="#abc837"/>
<path
android:pathData="m1056.3,481.23a12.72,12.72 0,0 1,-2.72 7.87,12.74 12.74,0 1,1 -20,0c-1.72,-2.17 22.72,-9.3 22.72,-7.87z"
android:strokeAlpha="0.1"
android:fillColor="#55d400"
android:fillAlpha="0.1"/>
</vector>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipe_refresh"
android:orientation="vertical"
android:layout_width="match_parent"
@@ -19,6 +20,43 @@
android:visibility="invisible"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/permissionsCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin"
android:layout_marginRight="@dimen/activity_margin"
android:layout_marginBottom="@dimen/card_padding"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/card_padding"
android:orientation="horizontal">
<TextView
android:id="@+id/permissionsText"
android:layout_height="wrap_content"
android:layout_width="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/permissionsBtn"
app:layout_constraintBottom_toBottomOf="parent"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/account_caldav_missing_permissions" />
<Button
android:id="@+id/permissionsBtn"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/permissionsText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:text="@string/account_permissions_action"
android:onClick="startPermissionsActivity"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<!-- paddingBottom and clipToPadding are needed to make space for the FAB at the end -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"

View File

@@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="model" type="at.bitfire.davdroid.ui.PermissionsFragment.Model"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/appIntro2BottomBarHeight">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/card_padding">
<at.bitfire.davdroid.ui.widget.CropImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_bias="0"
android:maxHeight="@dimen/card_theme_max_height"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/heading"
android:adjustViewBounds="true"
app:verticalOffsetPercent=".45"
app:srcCompat="@drawable/intro_permissions"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/card_padding" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="@dimen/card_padding" />
<TextView
android:id="@+id/heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/image"
app:layout_constraintBottom_toTopOf="@id/text"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/end"
android:layout_marginTop="@dimen/card_margin_title_text"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:text="@string/permissions_title" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/heading"
app:layout_constraintBottom_toTopOf="@id/contactsHeading"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/end"
android:layout_marginTop="@dimen/card_margin_title_text"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_text" />
<TextView
android:id="@+id/allHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/text"
app:layout_constraintBottom_toTopOf="@id/allStatus"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/allSwitch"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_all_title" />
<TextView
android:id="@+id/allStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/allHeading"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/allSwitch"
style="@style/TextAppearance.MaterialComponents.Body2"
android:text="@{model.haveAllPermissions ? @string/permissions_all_status_on : @string/permissions_all_status_off}" />
<Switch
android:id="@+id/allSwitch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="@id/allHeading"
app:layout_constraintBottom_toBottomOf="@id/allStatus"
app:layout_constraintEnd_toStartOf="@id/end"
android:clickable="@{!model.haveAllPermissions}"
android:checked="@={model.needAllPermissions}" />
<TextView
android:id="@+id/contactsHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/allStatus"
app:layout_constraintBottom_toTopOf="@id/contactsStatus"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/contactsSwitch"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_contacts_title" />
<TextView
android:id="@+id/contactsStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/contactsHeading"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/contactsSwitch"
style="@style/TextAppearance.MaterialComponents.Body2"
android:text="@{model.haveContactsPermissions ? @string/permissions_contacts_status_on : @string/permissions_contacts_status_off}" />
<Switch
android:id="@+id/contactsSwitch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="@id/contactsHeading"
app:layout_constraintBottom_toBottomOf="@id/contactsStatus"
app:layout_constraintEnd_toStartOf="@id/end"
android:clickable="@{!model.haveContactsPermissions}"
android:checked="@={model.needContactsPermissions}" />
<TextView
android:id="@+id/calendarHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
app:layout_constraintTop_toBottomOf="@id/contactsStatus"
app:layout_constraintBottom_toTopOf="@id/calendarStatus"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/calendarSwitch"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_calendar_title" />
<TextView
android:id="@+id/calendarStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/calendarHeading"
app:layout_constraintBottom_toTopOf="@id/tasksHeading"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/calendarSwitch"
style="@style/TextAppearance.MaterialComponents.Body2"
android:text="@{model.haveCalendarPermissions ? @string/permissions_calendar_status_on : @string/permissions_calendar_status_off}" />
<Switch
android:id="@+id/calendarSwitch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="@id/calendarHeading"
app:layout_constraintBottom_toBottomOf="@id/calendarStatus"
app:layout_constraintEnd_toStartOf="@id/end"
android:clickable="@{!model.haveCalendarPermissions}"
android:checked="@={model.needCalendarPermissions}" />
<TextView
android:id="@+id/tasksHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
app:layout_constraintTop_toBottomOf="@id/calendarStatus"
app:layout_constraintBottom_toTopOf="@id/tasksStatus"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/tasksSwitch"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_tasks_title" />
<TextView
android:id="@+id/tasksStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tasksHeading"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/tasksSwitch"
style="@style/TextAppearance.MaterialComponents.Body2"
android:text="@{model.haveTasksPermissions != null ? (model.haveTasksPermissions ? @string/permissions_tasks_status_on : @string/permissions_tasks_status_off) : @string/permissions_tasks_status_not_installed}" />
<Switch
android:id="@+id/tasksSwitch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="@id/tasksHeading"
app:layout_constraintBottom_toBottomOf="@id/tasksStatus"
app:layout_constraintEnd_toStartOf="@id/end"
android:enabled="@{model.haveTasksPermissions != null}"
android:clickable="@{!model.haveTasksPermissions}"
android:checked="@={model.needTasksPermissions}" />
<TextView
android:id="@+id/appSettingsHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tasksSwitch"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintEnd_toEndOf="@id/end"
android:layout_marginTop="16dp"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/permissions_app_settings_hint"/>
<Button
android:id="@+id/appSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/appSettingsHint"
app:layout_constraintStart_toStartOf="@id/start"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:text="@string/permissions_app_settings" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
</FrameLayout>
</layout>

View File

@@ -38,7 +38,7 @@
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Regular sync intervals"
android:text="@string/intro_battery_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/batteryStatus"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -69,7 +69,6 @@
app:layout_constraintEnd_toStartOf="@id/install"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:text="@string/intro_tasks_title" />
<TextView
android:id="@+id/status"
android:layout_width="0dp"
@@ -79,8 +78,7 @@
app:layout_constraintStart_toStartOf="@id/heading"
app:layout_constraintEnd_toEndOf="@id/heading"
style="@style/TextAppearance.MaterialComponents.Subtitle1"
android:text="@string/intro_tasks_not_installed" />
android:text="@{model.isInstalled() ? @string/intro_tasks_opentasks_installed : @string/intro_tasks_not_installed}" />
<Switch
android:id="@+id/install"
android:layout_width="wrap_content"
@@ -89,7 +87,6 @@
android:checked="@={model.shallBeInstalled}"
app:layout_constraintTop_toTopOf="@id/heading"
app:layout_constraintBottom_toBottomOf="@id/status"
app:layout_constraintStart_toEndOf="@id/heading"
app:layout_constraintEnd_toEndOf="@id/end"/>
<TextView

View File

@@ -57,7 +57,8 @@
<item
android:id="@+id/nav_donate"
android:icon="@drawable/ic_attach_money_dark"
android:title="@string/navigation_drawer_donate"/>
android:title="@string/navigation_drawer_donate"
android:visible="false"/>
<item
android:id="@+id/nav_privacy"
android:icon="@drawable/ic_cloud_off_dark"

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">أخطاء الشبكة و عمليات الإدخال/الإخراج</string>
<string name="notification_channel_sync_io_errors_desc">أوقات المهل، مشاكل الاتصال ، …الخ (مؤقتة في العادة)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">المكتبات</string>
<string name="about_version">النسخة %1$s (%2$d)</string>

View File

@@ -15,6 +15,7 @@
<string name="notification_channel_sync_io_errors">Грешки с входа/изхода и мрежата.</string>
<string name="notification_channel_sync_io_errors_desc">Проточване на времето, проблеми с връзката, и т.н. (обикновенно временни)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Библиотеки</string>
<string name="about_version">Версия %1$s (%2$d)</string>

View File

@@ -12,6 +12,7 @@
<string name="notification_channel_sync">Sincronització</string>
<string name="notification_channel_sync_io_errors">Xarxa i errors E/S</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->

View File

@@ -28,6 +28,7 @@
<string name="intro_autostart_text">Toto zařízení nejspíš synchronizaci blokuje. Pokud se vás to týká, je možné to vyřešit pouze ručně.</string>
<string name="intro_autostart_dont_show">Potřebná nastavení provedena, už nepřipomínat.*</string>
<string name="intro_leave_unchecked">* Pro připomenutí později nezaškrtávejte. Je možné resetovat v nastavení aplikace / %s.</string>
<string name="intro_more_info">Další informace</string>
<string name="intro_tasks_title">Podpora úkolů</string>
<string name="intro_tasks_not_installed">Nenainstalováno</string>
<string name="intro_tasks_opentasks_installed">OpenTasks nainstalováno</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Jsme rádi, že %s používáte. Jde o opensource software. Vývoj, údržba a podpora je ale těžká práce. Prosím zvažte zapojení se (je mnoho způsobů jak) nebo podpoření vývoje darem. Bude to velmi oceněno!</string>
<string name="intro_open_source_details">Jak se zapojit / podpořit vývoj darem</string>
<string name="intro_open_source_dont_show">Příště už nezobrazovat</string>
<string name="intro_more_info">Další informace</string>
<!--PermissionsActivity-->
<string name="permissions_title">Oprávnění</string>
<string name="permissions_text">Aby fungovalo správně, %s vyžaduje oprávnění.</string>
<string name="permissions_all_title">Vše níže uvedené</string>
<string name="permissions_all_status_off">Použít toto pro zapnutí všech funkcí (doporučeno)</string>
<string name="permissions_all_status_on">Všechna oprávnění udělena</string>
<string name="permissions_contacts_title">Oprávnění pro přístup ke kontaktům</string>
<string name="permissions_contacts_status_off">Bez synchronizace kontaktů (nedoporučeno)</string>
<string name="permissions_contacts_status_on">Synchronizace kontaktů možná</string>
<string name="permissions_calendar_title">Oprávnění pro přístup ke kalendáři</string>
<string name="permissions_calendar_status_off">Bez synchronizace kalendáře (nedoporučeno)</string>
<string name="permissions_calendar_status_on">Synchronizace kalendáře možná</string>
<string name="permissions_tasks_title">Oprávnění pro OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Bez synchronizace úkolů (nenainstalováno)</string>
<string name="permissions_tasks_status_off">Bez synchronizace úkolů (nedoporučeno)</string>
<string name="permissions_tasks_status_on">Synchronizace úkolů možná</string>
<string name="permissions_app_settings_hint">Pokud přepínač nefunguje, použijte nastavení aplikací / Oprávnění</string>
<string name="permissions_app_settings">Nastavení aplikace</string>
<!--AboutActivity-->
<string name="about_translations">Překlady</string>
<string name="about_libraries">Knihovny</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Bez synchronizace kontaktů (chybí oprávnění)</string>
<string name="account_caldav_missing_calendar_permissions">Bez synchronizace kalendáře (chybí oprávnění)</string>
<string name="account_caldav_missing_tasks_permissions">Žádné úkoly k synchronizaci (chybí oprávnění)</string>
<string name="account_caldav_missing_permissions">Bez synchronizace kalendáře a úkolů (chybí oprávnění)</string>
<string name="account_webcal_missing_calendar_permissions">Nedaří se přistupovat ke kalendářům (chybí oprávnění)</string>
<string name="account_permissions_action">Oprávnění</string>
<string name="account_no_address_books">(Zatím) zde nejsou žádné adresáře kontaktů.</string>
<string name="account_no_calendars">(Zatím) zde nejsou žádné kalendáře.</string>
<string name="account_no_webcals">Nejsou zde žádná přihlášení k odběru kalendáře (zatím).</string>

View File

@@ -17,25 +17,51 @@
<string name="notification_channel_sync_io_errors">Netværks- og I/O-fejl</string>
<string name="notification_channel_sync_io_errors_desc">Tidsudløb, forbindelse problemer m.m. (ofte midlertidig)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Dine data. Dit valg.</string>
<string name="intro_slogan2">Tag kontrol</string>
<string name="intro_battery_title">Regelmæssigt synkroniseringsinterval</string>
<string name="intro_battery_not_whitelisted">Deaktiveret (ikke anbefalet)</string>
<string name="intro_battery_whitelisted">Aktiveret (anbefalet)</string>
<string name="intro_battery_text">For regelmæssig synkroniseringsinterval, %s skal have tilladelse til at køre i baggrunden. Ellers kan Android til enhver tid pause synkronisering.</string>
<string name="intro_battery_dont_show">Ingen regelmæssigt synkroniseringsinterval.*</string>
<string name="intro_autostart_title">%s kompatibilitet</string>
<string name="intro_autostart_text">Enheden blokerer sandsynligvis synkronisering. Hvis dette er tilfældet kan det kun løses manuelt.</string>
<string name="intro_autostart_dont_show">Den krævede opsætning er udført. Stop påmindelser. *</string>
<string name="intro_leave_unchecked">* Efterlad åben for senere påmindelse. Kan nulstilles i program opsætning / %s.</string>
<string name="intro_more_info">Mere information</string>
<string name="intro_tasks_title">Opgaver understøttelse</string>
<string name="intro_tasks_not_installed">Ikke installeret</string>
<string name="intro_tasks_opentasks_installed">OpenTasks installeret</string>
<string name="intro_tasks_no_app_store">ingen program-butik tilgængelig</string>
<string name="intro_tasks_text1"><![CDATA[Android har ikke en indbygget opgaver understøttelse. Hvis opgaver er server understøttet, %s kan synkronisere dem va <a href="https://opentasks.app/">OpenTasks</a>.]]></string>
<string name="intro_tasks_text2">For avancerede funktioner som underopgaver eller gentagne opgaver, bruges 3. part program.</string>
<string name="intro_tasks_dont_show">Ingen opgaver understøttelse.*</string>
<string name="intro_open_source_title">Åben kilde program</string>
<string name="intro_open_source_text">Vi er glade for at %s bruges, som er åben kilde program. Udvikling, vedligehold og understøttelse er hårdt arbejde. Overvej at bidrage (der er flere måder) eller donere. Det vil blive meget værdsat!</string>
<string name="intro_open_source_details">Hvordan man bidrager/donerer</string>
<string name="intro_open_source_dont_show">Hvis ikke næste gang</string>
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">Oversættelser</string>
<string name="about_libraries">Biblioteker</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Oversat den %s</string>
<string name="about_flavor_info">Denne version er kun berettiget til distribution via Google Play.</string>
<string name="about_license_info_no_warranty">Dette program leveres ABSOLUT UDEN GARANTI. Det er fri software, og du er velkommen til at videredistribuere det under visse betingelse.</string>
<string name="about_translations_thanks"><![CDATA[<i>tak til: </i> %s]]></string>
<!--global settings-->
<string name="logging_couldnt_create_file">Kan ikke oprette log fil</string>
<string name="logging_notification_title">DAVx⁵ logning</string>
<string name="logging_notification_text">Logger nu alle DAVx⁵ aktiviteter</string>
<string name="logging_notification_send_log">Sende log</string>
<string name="logging_notification_send_log">Send log</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Åbne navigationsvindue</string>
<string name="navigation_drawer_close">Luk navigationsvindue</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV synkroniseringsadapter</string>
<string name="navigation_drawer_about">Om / licens</string>
<string name="navigation_drawer_beta_feedback">Beta tilbagemelding</string>
<string name="install_email_client">Installere en e-post klient</string>
<string name="install_browser">Installere en netlæser</string>
<string name="install_email_client">Installere e-post klient</string>
<string name="install_browser">Installere netlæser</string>
<string name="navigation_drawer_settings">Opsætning</string>
<string name="navigation_drawer_news_updates">Nyheder &amp; opdateringer</string>
<string name="navigation_drawer_external_links">Eksterne henvisninger</string>
@@ -55,7 +81,7 @@
<!--AppSettingsActivity-->
<string name="app_settings">Indstillinger</string>
<string name="app_settings_debug">Fejlsøgning</string>
<string name="app_settings_show_debug_info">Vis fejlsøgnings information</string>
<string name="app_settings_show_debug_info">Vis fejlsøgning information</string>
<string name="app_settings_show_debug_info_details">Vis/del software og opsætningsoplysninger</string>
<string name="app_settings_logging">Uddybende logning</string>
<string name="app_settings_logging_on">Logning er aktiv</string>
@@ -131,6 +157,8 @@
<string name="login_account_name_required">Kontonavn påkrævet</string>
<string name="login_account_name_already_taken">Konto navn er taget</string>
<string name="login_account_not_created">Konto kunne ikke oprettes</string>
<string name="login_webview_tlserror">Forbindelse sikkerheds problem (fejlkode %d)</string>
<string name="login_webview_retry">Prøv igen</string>
<string name="login_configuration_detection">Check konfiguration</string>
<string name="login_querying_server">Vent, forespørger serveren…</string>
<string name="login_no_caldav_carddav">Kunne ikke finde CalDAV- eller CardDAV-tjeneste.</string>

View File

@@ -26,20 +26,38 @@
<string name="intro_battery_dont_show">Ich brauche keine regelmäßigen Sync-Intervalle.*</string>
<string name="intro_autostart_title">%s-Kompatibilität</string>
<string name="intro_autostart_text">Dieses Gerät blockiert die Synchronisierung wahrscheinlich. Dies kann nur manuell behoben werden.</string>
<string name="intro_autostart_dont_show">Ich habe die Einstellungen gemacht, nicht mehr erinnern.</string>
<string name="intro_autostart_dont_show">Ich habe die Einstellungen gemacht, nicht mehr erinnern.*</string>
<string name="intro_leave_unchecked">* Nicht anwählen, um später erinnert zu werden. Kann unter App-Einstellungen / %s zurückgesetzt werden.</string>
<string name="intro_more_info">Mehr Infos</string>
<string name="intro_tasks_title">Unterstützung für Aufgaben</string>
<string name="intro_tasks_not_installed">Nicht installiert</string>
<string name="intro_tasks_opentasks_installed">OpenTasks installiert</string>
<string name="intro_tasks_opentasks_installed">Aufgaben installiert</string>
<string name="intro_tasks_no_app_store">Kein App-Store verfügbar</string>
<string name="intro_tasks_text1"><![CDATA[Android unterstützt standardmäßig keine Aufgaben. Wenn Aufgaben vom Server unterstützt werden, kann %s sie über <a href="https://opentasks.app/">OpenTasks</a> synchronisieren.]]></string>
<string name="intro_tasks_text1"><![CDATA[Android unterstützt standardmäßig keine Aufgaben. Wenn Aufgaben vom Server unterstützt werden, kann %s sie über <a href="https://opentasks.app/">OpenTasks</a> (Aufgaben) synchronisieren.]]></string>
<string name="intro_tasks_text2">Für bestimmte Funktionen wie Unteraufgaben oder Wiederholungen benötigen Sie zusätzliche Apps.</string>
<string name="intro_tasks_dont_show">Ich brauche keine Unterstützung für Aufgaben.</string>
<string name="intro_open_source_title">Open-Source-Software</string>
<string name="intro_open_source_text">Wir freuen uns, dass sie die Open-Source-Software %s verwenden. Entwicklung, Wartung und Support sind viel Arbeit. Ziehen Sie daher bitte in Betracht, mitzuhelfen (dazu gibt es viele Möglichkeiten) oder zu spenden. Vielen Dank!</string>
<string name="intro_open_source_text">Wir freuen uns, dass Sie die Open-Source-Software %s verwenden. Entwicklung, Wartung und Support sind viel Arbeit. Ziehen Sie daher bitte in Betracht, mitzuhelfen (dazu gibt es viele Möglichkeiten) oder zu spenden. Vielen Dank!</string>
<string name="intro_open_source_details">Infos zum Mithelfen/Spenden</string>
<string name="intro_open_source_dont_show">In nächster Zeit nicht anzeigen</string>
<string name="intro_more_info">Mehr Infos</string>
<string name="intro_open_source_dont_show">In nächster Zeit nicht mehr anzeigen</string>
<!--PermissionsActivity-->
<string name="permissions_title">Rechteverwaltung</string>
<string name="permissions_text">%s benötigt Rechte, um ordnungsgemäß zu funktionieren</string>
<string name="permissions_all_title">Alles darunter</string>
<string name="permissions_all_status_off">Benutze dies, um alle Funktionen einzuschalten (empfohlen)</string>
<string name="permissions_all_status_on">Allen Rechten zugestimmt</string>
<string name="permissions_contacts_title">Kontakte Rechteverwaltung</string>
<string name="permissions_contacts_status_off">Keine Kontaktsynchronisierung (nicht empfohlen)</string>
<string name="permissions_contacts_status_on">Kontaktsynchronisierung möglich</string>
<string name="permissions_calendar_title">Kalender Rechteverwaltung</string>
<string name="permissions_calendar_status_off">Keine Kalendersynchronisierung (nicht empfohlen)</string>
<string name="permissions_calendar_status_on">Kalendersynchronisierung möglich</string>
<string name="permissions_tasks_title">Aufgaben Rechteverwaltung</string>
<string name="permissions_tasks_status_not_installed">Keine Aufgabensynchronisierung (nicht installiert)</string>
<string name="permissions_tasks_status_off">Keine Aufgabensynchronisierung (nicht empfohlen)</string>
<string name="permissions_tasks_status_on">Aufgabensynchronisierung möglich</string>
<string name="permissions_app_settings_hint">Wenn ein Schalter nicht funktioniert, versuche es unter Einstellungen -&gt; Apps &amp; Benachrichtigungen</string>
<string name="permissions_app_settings">App Einstellungen</string>
<!--AboutActivity-->
<string name="about_translations">Übersetzungen</string>
<string name="about_libraries">Bibliotheken</string>
@@ -59,7 +77,7 @@
<string name="navigation_drawer_subtitle">CalDAV/CardDAV-Sync-Adapter</string>
<string name="navigation_drawer_about">Über / Lizenz</string>
<string name="navigation_drawer_beta_feedback">Beta-Rückmeldung</string>
<string name="install_email_client">Bitte installieren Sie eine Email-Anwendung</string>
<string name="install_email_client">Bitte installieren Sie eine E-Mail-Anwendung</string>
<string name="install_browser">Bitte installieren Sie einen Web-Browser</string>
<string name="navigation_drawer_settings">Einstellungen</string>
<string name="navigation_drawer_news_updates">Aktuelles</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Keine Kontaktsynchronisierung (fehlende Rechte)</string>
<string name="account_caldav_missing_calendar_permissions">Keine Kalendersynchronisierung (fehlende Rechte)</string>
<string name="account_caldav_missing_tasks_permissions">Keine Aufgabensynchronisierung (fehlende Rechte)</string>
<string name="account_caldav_missing_permissions">Keine Kalender- und Kontaktsynchronisierung (fehlende Rechte)</string>
<string name="account_webcal_missing_calendar_permissions">Fehlender Zugang zu Kalendern (fehlende Rechte)</string>
<string name="account_permissions_action">Rechteverwaltung</string>
<string name="account_no_address_books">(Noch) keine Adressbücher vorhanden</string>
<string name="account_no_calendars">(Noch) keine Kalender vorhanden</string>
<string name="account_no_webcals">(Noch) keine Kalender-Abos vorhanden</string>
@@ -121,7 +145,7 @@
<string name="account_delete">Konto löschen</string>
<string name="account_delete_confirmation_title">Konto wirklich löschen?</string>
<string name="account_delete_confirmation_text">Alle Adressbücher, Kalender und Aufgabenlisten werden vom Gerät (nicht am Server) gelöscht.</string>
<string name="account_synchronize_this_collection">diesen Ordner synchronisieren</string>
<string name="account_synchronize_this_collection">Diesen Ordner synchronisieren</string>
<string name="account_read_only">schreibgeschützt</string>
<string name="account_calendar">Kalender</string>
<string name="account_task_list">Aufgabenliste</string>
@@ -186,9 +210,9 @@
<string name="settings_sync_wifi_only_ssids_on">Synchronisierung nur über %s</string>
<string name="settings_sync_wifi_only_ssids_on_location_services">Synchronisierung nur über %s (benötigt aktive Standortdienste)</string>
<string name="settings_sync_wifi_only_ssids_off">Alle WLAN-Verbindungen werden verwendet</string>
<string name="settings_sync_wifi_only_ssids_message">Erlaubte WLAN-Namen (SSIDs, mit Beistrich getrennt, leer lassen für alle)</string>
<string name="settings_sync_wifi_only_ssids_message">Erlaubte WLAN-Namen (SSIDs, mit Komma getrennt, leer lassen für alle)</string>
<string name="settings_sync_wifi_only_ssids_location_permission">Zum Auslesen von WLAN-Namen sind die Standort-Berechtigung und die ständige Aktivierung der Standortdienste notwendig.</string>
<string name="settings_more_info_faq">Mehr Info (FAQ)</string>
<string name="settings_more_info_faq">Mehr Infos (FAQ)</string>
<string name="settings_authentication">Anmeldeinformationen</string>
<string name="settings_username">Benutzername</string>
<string name="settings_enter_username">Benutzername eingeben:</string>
@@ -263,7 +287,7 @@
<string name="sync_contacts_read_only_address_book">Schreibgeschütztes Adressbuch</string>
<string name="sync_error_permissions">DAVx⁵-Berechtigungen</string>
<string name="sync_error_permissions_text">Zusätzliche Berechtigungen benötigt</string>
<string name="sync_error_opentasks_too_old">OpenTasks zu alt</string>
<string name="sync_error_opentasks_too_old">OpenTasks (Aufgaben) zu alt</string>
<string name="sync_error_opentasks_required_version">Version %1$s benötigt (derzeit %2$s)</string>
<string name="sync_error_authentication_failed">Anmeldungsfehler (Login-Daten überprüfen)</string>
<string name="sync_error_io">Netzwerk- oder E/A-Fehler %s</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Σφάλματα δικτύου και I/O</string>
<string name="notification_channel_sync_io_errors_desc">Χρονικά όρια, προβλήματα σύνδεσης, κλπ. (συχνά προσωρινά)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Βιβλιοθήκες</string>
<string name="about_version">Έκδοση %1$s (%2$d)</string>

View File

@@ -28,6 +28,7 @@
<string name="intro_autostart_text">Este dispositivo bloquea la sincronización. Si está afectado, sólo lo puede resolver manualmente.</string>
<string name="intro_autostart_dont_show">No tengo los ajustes requeridos. No volver a recordar.*</string>
<string name="intro_leave_unchecked">* Déjelo desmarcado para que se le recuerde más tarde. Se puede reconfigurar en los ajustes de la aplicación / %s</string>
<string name="intro_more_info">Información adicional</string>
<string name="intro_tasks_title">Soporte de tareas</string>
<string name="intro_tasks_not_installed">No instalado</string>
<string name="intro_tasks_opentasks_installed">OpenTasks instalado</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Nos complace que use%s, que es software open-source. Desarrollar, mantener y asistir a usuarios es un trabajo duro. Por favor, considere contribuir (hay muchas maneras) o hacer una donación. ¡Se agradecería mucho!</string>
<string name="intro_open_source_details">Cómo contribuir o donar</string>
<string name="intro_open_source_dont_show">No mostrar la próxima vez</string>
<string name="intro_more_info">Información adicional</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisos</string>
<string name="permissions_text">%s necesita permisos para funcionar correctamente.</string>
<string name="permissions_all_title">Todos los siguientes</string>
<string name="permissions_all_status_off">Usa esto para activar todas las características (recomendado)</string>
<string name="permissions_all_status_on">Todos los permisos concedidos</string>
<string name="permissions_contacts_title">Permisos de contactos</string>
<string name="permissions_contacts_status_off">No sincronizar contactos (no recomendado)</string>
<string name="permissions_contacts_status_on">Sincronización de contactos permitida</string>
<string name="permissions_calendar_title">Permisos de calendario</string>
<string name="permissions_calendar_status_off">Sin sincronización de calendario (no recomendado)</string>
<string name="permissions_calendar_status_on">Sincronización de calendario permitida</string>
<string name="permissions_tasks_title">Permisos de OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Sin sincronización de tareas (no instalado)</string>
<string name="permissions_tasks_status_off">Sin sincronización de tareas (no recomendado)</string>
<string name="permissions_tasks_status_on">Sincronización de tareas permitidas</string>
<string name="permissions_app_settings_hint">Si un interruptor no funciona usa Configuraciones de la app / Permisos.</string>
<string name="permissions_app_settings">Configuraciones de la app</string>
<!--AboutActivity-->
<string name="about_translations">Traducciones</string>
<string name="about_libraries">Bibliotecas</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Sin sincronización de contactos (faltan permisos)</string>
<string name="account_caldav_missing_calendar_permissions">Sin sincronización de calendario (faltan permisos)</string>
<string name="account_caldav_missing_tasks_permissions">Sin sincronización de tareas (faltan permisos)</string>
<string name="account_caldav_missing_permissions">Sin sincronización de calendario y tareas (faltan permisos)</string>
<string name="account_webcal_missing_calendar_permissions">No se puede acceder a los calendarios (faltan permisos)</string>
<string name="account_permissions_action">Permisos</string>
<string name="account_no_address_books">No hay libretas de direciones (todavía).</string>
<string name="account_no_calendars">No hay calendarios (todavía). </string>
<string name="account_no_webcals">No hay suscripciones a calendarios (todavía).</string>
@@ -156,6 +180,8 @@
<string name="login_account_name_required">Nombre de cuenta requerido</string>
<string name="login_account_name_already_taken">El nombre de la cuenta ya está siendo utilizado</string>
<string name="login_account_not_created">La cuenta no pudo ser creada</string>
<string name="login_webview_tlserror">Problema de seguridad en la conexión (código de error %d)</string>
<string name="login_webview_retry">Reintentar</string>
<string name="login_configuration_detection">Detectar configuración</string>
<string name="login_querying_server">Por favor espera, consultando al servidor…</string>
<string name="login_no_caldav_carddav">No se pudo encontrar el servicio CalDAV o CardDAV.</string>

View File

@@ -15,7 +15,28 @@
<string name="notification_channel_sync_warnings">Sinkronizazio abisuak</string>
<string name="notification_channel_sync_io_errors">Sare eta S/I erroreak</string>
<!--IntroActivity-->
<string name="intro_slogan1">Zure datuak. Zure aukera.</string>
<string name="intro_slogan2">Har ezazu kontrola.</string>
<string name="intro_battery_not_whitelisted">Desgaituta (ez gomendatuta)</string>
<string name="intro_battery_whitelisted">Gaituta (gomendatuta)</string>
<string name="intro_more_info">Informazio gehiago</string>
<string name="intro_tasks_not_installed">Instalatu gabe</string>
<string name="intro_tasks_opentasks_installed">OpenTasks instalatuta</string>
<string name="intro_tasks_no_app_store">Ez dago denda aplikaziorik eskuragarri </string>
<string name="intro_open_source_title">Kode irekiko softwarea</string>
<!--PermissionsActivity-->
<string name="permissions_title">Baimenak</string>
<string name="permissions_all_status_on">Baimen guztiak eman dira</string>
<string name="permissions_contacts_title">Kontaktuen baimenak</string>
<string name="permissions_contacts_status_off">Kontaktu sinkronizaziorik ez (ez gomendatuta)</string>
<string name="permissions_contacts_status_on">Kontaktuen sinkronizazioa posible</string>
<string name="permissions_calendar_title">Egutegiaren baimenak</string>
<string name="permissions_calendar_status_off">Egutegi sinkronizaziorik ez (ez gomendatuta)</string>
<string name="permissions_calendar_status_on">Egutegiaren sinkronizazioa posible</string>
<string name="permissions_tasks_title">OpenTasks baimenak</string>
<string name="permissions_app_settings">Aplikazioaren ezarpenak</string>
<!--AboutActivity-->
<string name="about_translations">Itzulpenak</string>
<string name="about_libraries">Liburutegiak</string>
<string name="about_version">%1$s (%2$d) bertsioa</string>
<string name="about_build_date">%s(e)an konpilatuta</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">خطاهای شبکه</string>
<string name="notification_channel_sync_io_errors_desc">اتمام وقت، مشکل اتصال، غیره(اغلب موقت)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">کتابخانه ها</string>
<string name="about_version">نسخه %1$s (%2$d)</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Verkko ja I/O virheet</string>
<string name="notification_channel_sync_io_errors_desc">Aikakatkaisut, yhteysvirheet, yms. (usein väliaikaisia)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->

View File

@@ -17,12 +17,38 @@
<string name="notification_channel_sync_io_errors">Erreurs de réseau et d\'entrée / sortie</string>
<string name="notification_channel_sync_io_errors_desc">Délais d\'attente, problèmes de connexion, etc. (souvent temporaires)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Vos données, votre choix.</string>
<string name="intro_slogan2">Prenez le contrôle.</string>
<string name="intro_battery_title">Intervalles de synchronisation régulières</string>
<string name="intro_battery_not_whitelisted">Désactiver (non recommandé)</string>
<string name="intro_battery_whitelisted">Activé (recommandé)</string>
<string name="intro_battery_text">Pour une synchronisation à intervalles régulières, %s doit être autorisé à fonctionner en arrière-plan. Sinon, Android peut interrompre la synchronisation à tout moment.</string>
<string name="intro_battery_dont_show">Je n\'ai pas besoin d\'intervalles de synchronisation régulières.*</string>
<string name="intro_autostart_title">%s compatibilité</string>
<string name="intro_autostart_text">Cet appareil bloque probablement la synchronisation. Si vous êtes affecté, vous ne pouvez résoudre ce problème que manuellement.</string>
<string name="intro_autostart_dont_show">J\'ai fait les réglages nécessaires. Ne me le rappelez plus.*</string>
<string name="intro_leave_unchecked">* Laisser non coché pour un rappel ultérieur. Peut être réinitialisé dans les paramètres de l\'application / %s.</string>
<string name="intro_more_info">Plus d\'informations</string>
<string name="intro_tasks_title">Support des tâches</string>
<string name="intro_tasks_not_installed">Non installé</string>
<string name="intro_tasks_opentasks_installed">OpenTasks installé</string>
<string name="intro_tasks_no_app_store">Pas de magasin d\'application disponible</string>
<string name="intro_tasks_text1"><![CDATA[Android n\'a pas de support intégré pour les tâches. Si les tâches sont prises en charge par votre serveur, %s peut les synchroniser en utilisant <a href="https://opentasks.app/">OpenTasks</a>.]]></string>
<string name="intro_tasks_text2">Pour les fonctionnalités avancées comme les sous-tâches ou les tâches récurrentes, vous aurez besoin d\'applications supplémentaires.</string>
<string name="intro_tasks_dont_show">Je n\'ai pas besoin de support des tâches.*</string>
<string name="intro_open_source_title">Logiciels open-source</string>
<string name="intro_open_source_text">Nous sommes heureux que vous utilisiez %s, qui est un logiciel open-source. Le développement, la maintenance et l\'assistance sont un travail difficile. Veuillez envisager de contribuer (il y a plusieurs façons de le faire) ou de faire un don. Ce serait très apprécié !</string>
<string name="intro_open_source_details">Comment contribuer/donner</string>
<string name="intro_open_source_dont_show">Ne pas afficher la prochaine fois</string>
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">Traductions</string>
<string name="about_libraries">Librairies</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_build_date">Compilé sur %s</string>
<string name="about_flavor_info">Cette version ne peut être distribuée que sur Google Play.</string>
<string name="about_license_info_no_warranty">Ce programme est fourni sans AUCUNE GARANTIE. C\'est un logiciel libre, et vous êtes en droit de le redistribuer sous certaines conditions.</string>
<string name="about_translations_thanks"><![CDATA[<i>Merci à : </i> %s]]></string>
<!--global settings-->
<string name="logging_couldnt_create_file">Impossible de créer le fichier journal</string>
<string name="logging_notification_title">Journalisation de DAVx⁵</string>
@@ -131,6 +157,8 @@
<string name="login_account_name_required">Nom du compte requis</string>
<string name="login_account_name_already_taken">Le nom du compte est déjà pris</string>
<string name="login_account_not_created">Le compte n\'a pas pu être créé</string>
<string name="login_webview_tlserror">Problème de sécurité de la connexion (code d\'erreur %d)</string>
<string name="login_webview_retry">Réessayer</string>
<string name="login_configuration_detection">Détection de la configuration</string>
<string name="login_querying_server">Veuillez patienter, nous interrogeons le serveur …</string>
<string name="login_no_caldav_carddav">Aucun accès possible au service CalDAV ou CardDAV.</string>

View File

@@ -28,6 +28,7 @@
<string name="intro_autostart_text">Este dispositivo probablemente está a bloquear a sincronización. Esto só se pode solucionar de xeito manual.</string>
<string name="intro_autostart_dont_show">Xa fixen o que me pediades. Non mo lembres máis.*</string>
<string name="intro_leave_unchecked">* Deixar sen marcar para lembrar máis tarde. Pode restablecerse nos axustes da app / %s.</string>
<string name="intro_more_info">Máis información</string>
<string name="intro_tasks_title">Soporte para Tasks</string>
<string name="intro_tasks_not_installed">Non instalada</string>
<string name="intro_tasks_opentasks_installed">OpenTasks instalada</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Encántanos que uses %s, que é software de código aberto. O desenvolvemento, mantemento e soporte son un traballo difícil. Considera contribuír (hai moitos xeitos) ou facer unha doazón. Sería de agradecer!</string>
<string name="intro_open_source_details">Como contribuír/doar</string>
<string name="intro_open_source_dont_show">Non mostar de novo</string>
<string name="intro_more_info">Máis información</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisos</string>
<string name="permissions_text">1%s require permisos para funcionar axeitadamente.</string>
<string name="permissions_all_title">Todos os de arriba</string>
<string name="permissions_all_status_off">Use isto para habilitar todas as características (recomendado)</string>
<string name="permissions_all_status_on">Todos os permisos concedidos</string>
<string name="permissions_contacts_title">Permisos de contactos</string>
<string name="permissions_contacts_status_off">Sen sincronización de contactos (non recomendado) </string>
<string name="permissions_contacts_status_on">É posible sincronizar os contactos</string>
<string name="permissions_calendar_title">Permisos de calendario</string>
<string name="permissions_calendar_status_off">Sen sincronización de calendario (non se recomenda)</string>
<string name="permissions_calendar_status_on">É posible sincronizar o calendario</string>
<string name="permissions_tasks_title">Permisos de OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Sen sincronización de tarefas (non instalado) </string>
<string name="permissions_tasks_status_off">Sen sincronización de tarefas (non se recomenda)</string>
<string name="permissions_tasks_status_on">É posible sincronizar as tarefas</string>
<string name="permissions_app_settings_hint">Se unha opción non funciona, use configuracións da aplicación / Permisos.</string>
<string name="permissions_app_settings">Permisos da aplicación</string>
<!--AboutActivity-->
<string name="about_translations">Traducións</string>
<string name="about_libraries">Bibliotecas</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Sen sincronización de contactos (faltan permisos)</string>
<string name="account_caldav_missing_calendar_permissions">Sen sincronización de calendario (faltan permisos)</string>
<string name="account_caldav_missing_tasks_permissions">Sen sincronización de tarefas (faltan permisos)</string>
<string name="account_caldav_missing_permissions">Sen sincronización de calendario e tarefas (faltan permisos)</string>
<string name="account_webcal_missing_calendar_permissions">Non se pode acceder aos calendarios (faltan permisos)</string>
<string name="account_permissions_action">Permisos</string>
<string name="account_no_address_books">No existen libretas de enderezos (aínda).</string>
<string name="account_no_calendars">Non existen calendarios (aínda).</string>
<string name="account_no_webcals">Non tes subscricións a calendario (aínda).</string>
@@ -156,6 +180,8 @@
<string name="login_account_name_required">Nome de conta requerido</string>
<string name="login_account_name_already_taken">O nome de conta xa está a ser utilizado</string>
<string name="login_account_not_created">Non se creou a conta</string>
<string name="login_webview_tlserror">Problema de seguridade de conexión (código de erro %d)</string>
<string name="login_webview_retry">Reintentar</string>
<string name="login_configuration_detection">Detección da configuración</string>
<string name="login_querying_server">Agarda por favor, consultando o servidor…</string>
<string name="login_no_caldav_carddav">Non se atopou servizo CalDAV ou CardDAV.</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Hálózati és I/O hibák</string>
<string name="notification_channel_sync_io_errors_desc">Időtúllépésék, kapcsolódási problémák, stb. (gyakran átmeneti problémák)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Könyvtárak</string>
<string name="about_version">Verziószám:%1$s (%2$d)</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Errori di Rete e di I/O</string>
<string name="notification_channel_sync_io_errors_desc">Timeouts, problemi di connessione, ecc. (spesso temporanei)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Librerie</string>
<string name="about_version">Versione %1$s (%2$d)</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">ネットワークおよび I/O エラー</string>
<string name="notification_channel_sync_io_errors_desc">タイムアウト、接続の問題など (多くの場合、一時的なもの)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">ライブラリー</string>
<string name="about_version">バージョン %1$s (%2$d)</string>

View File

@@ -15,6 +15,7 @@
<string name="notification_channel_sync_warnings_desc">Ikke-kritiske synkroniseringsproblem, som enkelte ugyldige filer</string>
<string name="notification_channel_sync_io_errors">Nettverk- og I/O-feil</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Bibliotek</string>
<string name="about_version">Versjon %1$s(%2$d)</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Network en I/O errors</string>
<string name="notification_channel_sync_io_errors_desc">Timeouts, connectie problemen, etc. (vaak tijdelijk).</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Libraries</string>
<string name="about_version">Versie%1$s(%2$d)</string>

View File

@@ -28,6 +28,7 @@
<string name="intro_autostart_text">To urządzenie prawdopodobnie blokuje synchronizację. Jeśli ten problem Cię dotyczy, możesz go jedynie rozwiązać ręcznie.</string>
<string name="intro_autostart_dont_show">Wprowadziłem potrzebne ustawienia. Nie przypominaj mi ponownie.*</string>
<string name="intro_leave_unchecked">* Pozostaw nie zaznaczone aby otrzymać przypomnienie później. Możesz to zresetować w ustawieniach aplikacji / %s. </string>
<string name="intro_more_info">Więcej informacji</string>
<string name="intro_tasks_title">Obsługa zadań</string>
<string name="intro_tasks_not_installed">Nie zainstalowane</string>
<string name="intro_tasks_opentasks_installed">OpenTasks zainstalowane</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Jesteśmy szczęśliwi, że używasz %s, który jest oprogramowaniem open-source. Rozwój, utrzymanie i wsparcie są ciężką pracą. Proszę rozważ wsparcie (jest na to wiele sposobów) lub dotację. Będziemy bardzo wdzięczni.</string>
<string name="intro_open_source_details">Jak wspomóc/wesprzeć</string>
<string name="intro_open_source_dont_show">Nie pokazuj za kolejnym razem</string>
<string name="intro_more_info">Więcej informacji</string>
<!--PermissionsActivity-->
<string name="permissions_title">Uprawnienia</string>
<string name="permissions_text">%s wymaga uprawnień aby działać prawidłowo.</string>
<string name="permissions_all_title">Wszystkie poniższe</string>
<string name="permissions_all_status_off">Użyj tego aby odblokować wszystkie funkcje (zalecane)</string>
<string name="permissions_all_status_on">Wszystkie uprawnienia nadane</string>
<string name="permissions_contacts_title">Uprawnienia kontaktów</string>
<string name="permissions_contacts_status_off">Bez synchronizacji kontaktów (nie zalecane)</string>
<string name="permissions_contacts_status_on">Synchronizacja kontaktów możliwa</string>
<string name="permissions_calendar_title">Uprawnienia kalendarza</string>
<string name="permissions_calendar_status_off">Bez synchronizacji kalendarza (nie zalecane)</string>
<string name="permissions_calendar_status_on">Synchronizacja kalendarza możliwa</string>
<string name="permissions_tasks_title">Uprawnienia OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Bez synchronizacji zadań (nie zainstalowane)</string>
<string name="permissions_tasks_status_off">Bez synchronizacji zadań (nie zalecane)</string>
<string name="permissions_tasks_status_on">Synchronizacja zadań możliwa</string>
<string name="permissions_app_settings_hint">Jeśli przełącznik nie działa użyj ustawień aplikacji / Uprawnienia.</string>
<string name="permissions_app_settings">Ustawienia aplikacji</string>
<!--AboutActivity-->
<string name="about_translations">Tłumaczenia</string>
<string name="about_libraries">Biblioteki</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Bez synchronizacji kontaktów (brakuje uprawnień)</string>
<string name="account_caldav_missing_calendar_permissions">Bez synchronizacji kalendarza (brakuje uprawnień)</string>
<string name="account_caldav_missing_tasks_permissions">Bez synchronizacji zadań (brakuje uprawnień)</string>
<string name="account_caldav_missing_permissions">Bez synchronizacji kalendarza i zadań (brakuje uprawnień)</string>
<string name="account_webcal_missing_calendar_permissions">Brak dostępu do kalendarzy (brakuje uprawnień)</string>
<string name="account_permissions_action">Uprawnienia</string>
<string name="account_no_address_books">Nie ma (jeszcze) książek adresowych.</string>
<string name="account_no_calendars">Nie ma (jeszcze) kalendarzy.</string>
<string name="account_no_webcals">Nie ma (jeszcze) subskrypcji kalendarzy.</string>

View File

@@ -28,6 +28,7 @@
<string name="intro_autostart_text">Este dispositivo provavelmente bloqueia a sincronização. Se for afetado por isso, você só poderá resolver isso manualmente.</string>
<string name="intro_autostart_dont_show">Fiz as configurações necessárias. Não me lembre novamente.*</string>
<string name="intro_leave_unchecked">* Deixe desmarcado para ser lembrado mais tarde. Pode ser redefinido nas configurações do aplicativo / %s.</string>
<string name="intro_more_info">Mais informações</string>
<string name="intro_tasks_title">Suporte a tarefas</string>
<string name="intro_tasks_not_installed">Não instalado</string>
<string name="intro_tasks_opentasks_installed">OpenTasks instalado</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Estamos felizes por usar o %s, que é um software de código aberto. Desenvolvimento, manutenção e suporte são um trabalho árduo. Considere contribuir (existem várias maneiras) ou fazer uma doação. Seria muito apreciado!</string>
<string name="intro_open_source_details">Como contribuir/doar</string>
<string name="intro_open_source_dont_show">Não exibir na próxima vez</string>
<string name="intro_more_info">Mais informações</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permissões</string>
<string name="permissions_text">%s requer permissões para trabalhar corretamente.</string>
<string name="permissions_all_title">Todos os abaixo</string>
<string name="permissions_all_status_off">Use isto para ativar todas as funcionalidades (recomendado)</string>
<string name="permissions_all_status_on">Todas as permissões concedidas</string>
<string name="permissions_contacts_title">Permissões de contatos</string>
<string name="permissions_contacts_status_off">Nenhum contato sincronizado (não recomendado)</string>
<string name="permissions_contacts_status_on">Possível sincronização de contatos</string>
<string name="permissions_calendar_title">Permissões de calendário</string>
<string name="permissions_calendar_status_off">Nenhum calendário sincronizado (não recomendado)</string>
<string name="permissions_calendar_status_on">Possível sincronização de calendário</string>
<string name="permissions_tasks_title">Permissões do OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Nenhuma tarefa sincronizada (não instalado)</string>
<string name="permissions_tasks_status_off">Nenhuma tarefa sincronizada (não recomendado)</string>
<string name="permissions_tasks_status_on">Possível sincronização de tarefa</string>
<string name="permissions_app_settings_hint">Se uma opção não funcionar, use as configurações / permissões do aplicativo.</string>
<string name="permissions_app_settings">Configurações do aplicativo</string>
<!--AboutActivity-->
<string name="about_translations">Traduções</string>
<string name="about_libraries">Bibliotecas</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">Nenhuma sincronização de contatos (falta de permissões)</string>
<string name="account_caldav_missing_calendar_permissions">Nenhuma sincronização de calendário (falta de permissões)</string>
<string name="account_caldav_missing_tasks_permissions">Nenhuma sincronização de tarefas (falta de permissões)</string>
<string name="account_caldav_missing_permissions">Nenhuma sincronização de calendário e tarefas (falta de permissões)</string>
<string name="account_webcal_missing_calendar_permissions">Não é possível acessar calendários (falta de permissões)</string>
<string name="account_permissions_action">Permissões</string>
<string name="account_no_address_books">Não há livros de endereços (ainda).</string>
<string name="account_no_calendars">Não há calendários (ainda).</string>
<string name="account_no_webcals">Não há assinaturas de calendário (ainda).</string>

View File

@@ -4,6 +4,7 @@
<string name="app_name">DAVDroid</string>
<string name="help">Ajuda</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->

View File

@@ -28,6 +28,7 @@
<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>
<string name="intro_tasks_title">Поддержка задач</string>
<string name="intro_tasks_not_installed">Не установлено</string>
<string name="intro_tasks_opentasks_installed">OpenTasks установлен</string>
@@ -39,7 +40,24 @@
<string name="intro_open_source_text">Мы рады, что вы используете %s, программное обеспечение с открытым исходным кодом. Разработка, сопровождение и поддержка - это тяжелая работа. Пожалуйста, подумайте о том, чтобы внести свой вклад (существует множество способов) или сделать пожертвование. Будем очень признательны!</string>
<string name="intro_open_source_details">Как внести свой вклад/пожертвовать</string>
<string name="intro_open_source_dont_show">Не показывать в следующий раз</string>
<string name="intro_more_info">Дополнительная информация</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_tasks_title">Разрешения OpenTasks</string>
<string name="permissions_tasks_status_not_installed">Не синхронизировать задачи (не установлены)</string>
<string name="permissions_tasks_status_off">Не синхронизировать задачи (не рекомендуется)</string>
<string name="permissions_tasks_status_on">Возможна синхронизация задач</string>
<string name="permissions_app_settings_hint">Если переключатель не работает, используйте настройки приложения / разрешения.</string>
<string name="permissions_app_settings">Настройки приложения</string>
<!--AboutActivity-->
<string name="about_translations">Переводы</string>
<string name="about_libraries">Библиотеки</string>
@@ -108,6 +126,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
<string name="account_carddav_missing_permissions">Не синхронизировать контакты (отсутствуют разрешения)</string>
<string name="account_caldav_missing_calendar_permissions">Не синхронизировать календарь (отсутствуют разрешения)</string>
<string name="account_caldav_missing_tasks_permissions">Не синхронизировать задачи (отсутствуют разрешения)</string>
<string name="account_caldav_missing_permissions">Не синхронизировать календарь и задачи (отсутствуют разрешения)</string>
<string name="account_webcal_missing_calendar_permissions">Нет доступа к календарям (отсутствуют разрешения)</string>
<string name="account_permissions_action">Разрешения</string>
<string name="account_no_address_books">Нет адресных книг (пока).</string>
<string name="account_no_calendars">Нет календарей (пока).</string>
<string name="account_no_webcals">Нет подписок на календарь (пока).</string>
@@ -133,29 +157,31 @@
<string name="account_install_icsx5">Установить ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Добавить аккаунт</string>
<string name="login_type_email">Войти по email</string>
<string name="login_email_address">Адрес электронной почты</string>
<string name="login_email_address_error">Требуется действующий адрес электронной почты</string>
<string name="login_type_email">Войти c адресом email</string>
<string name="login_email_address">Адрес email</string>
<string name="login_email_address_error">Требуется действительный адрес email</string>
<string name="login_password">Пароль</string>
<string name="login_password_required">Требуется пароль</string>
<string name="login_type_url">Войти по URL-адресу и имени пользователя</string>
<string name="login_type_url">Войти с URL и именем пользователя</string>
<string name="login_url_must_be_http_or_https">URL должен начинаться с http(s)://</string>
<string name="login_url_must_be_https">URL-адрес должен начинаться с https://</string>
<string name="login_url_must_be_https">URL должен начинаться с https://</string>
<string name="login_url_host_name_required">Требуется имя хоста</string>
<string name="login_user_name">Имя пользователя</string>
<string name="login_user_name_required">Требуется Имя пользователя</string>
<string name="login_base_url">Базовый URL</string>
<string name="login_type_url_certificate">Войти по URL-адресу и клиентскому сертификату</string>
<string name="login_type_url_certificate">Войти с URL и сертификатом клиента</string>
<string name="login_select_certificate">Выберите сертификат</string>
<string name="login_login">Войти</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Создать аккаунт</string>
<string name="login_account_name">Имя аккаунта</string>
<string name="login_account_name_info">Используйте ваш адрес адрес электронной почты в качестве имени аккаунта, поскольку Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые вы создаете. У вас не может быть двух аккаунтов с тем же именем.</string>
<string name="login_account_name_info">Укажите ваш адрес email в качестве названия аккаунта, поскольку Android будет его использовать в поле ORGANIZER для создаваемых событий. У вас не может быть двух аккаунтов с тем же именем.</string>
<string name="login_account_contact_group_method">Метод группировки контактов:</string>
<string name="login_account_name_required">Требуется имя аккаунта</string>
<string name="login_account_name_already_taken">Имя аккаунта уже используется</string>
<string name="login_account_not_created">Аккаунт не может быть создан</string>
<string name="login_webview_tlserror">Проблема безопасности соединения (код ошибки %d)</string>
<string name="login_webview_retry">Повторить</string>
<string name="login_configuration_detection">Обнаружение конфигурации</string>
<string name="login_querying_server">Ожидайте, выполняется запрос к серверу…</string>
<string name="login_no_caldav_carddav">Не удалось найти службу CalDAV или CardDAV.</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Sieťové a V/V chyby</string>
<string name="notification_channel_sync_io_errors_desc">Vypršanie času, problémy spojenia, atď. (často dočasné)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Knižnice</string>
<string name="about_version">Verzia %1$s (%2$d)</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Omrežje in I/O napake</string>
<string name="notification_channel_sync_io_errors_desc">Pavze, povezave v povezavi z omrežjem, itd. (ponavadi začasno)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Knjižnice </string>
<string name="about_version">Verzija %1$s (%2$d)</string>

View File

@@ -13,6 +13,7 @@
<string name="notification_channel_sync_errors">Грешке синхронизације</string>
<string name="notification_channel_sync_io_errors">Мрежне и У/И грешке</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Библиотеке</string>
<string name="about_version">Издање %1$s (%2$d)</string>

View File

@@ -17,12 +17,38 @@
<string name="notification_channel_sync_io_errors">Błyndy necu i wchodu/wychodu</string>
<string name="notification_channel_sync_io_errors_desc">Braki ôdpowiedzi, problymy połōnczynio, itp. (z wiynksza tymczasowe)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Twoje dane. Twōj wybōr.</string>
<string name="intro_slogan2">Przejmij kōntrola.</string>
<string name="intro_battery_title">Regularne interwały synchrōnizacyje</string>
<string name="intro_battery_not_whitelisted">Zastawiōne (niyrekōmyndowane)</string>
<string name="intro_battery_whitelisted">Włōnczōne (rekōmyndowane)</string>
<string name="intro_battery_text">Żeby regularnie synchrōnizować, %s musi mieć zwolo na fungowanie na zadku. W inkszym przipadku Android może w kożdyj chwili zastawić synchrōnizacyjo.</string>
<string name="intro_battery_dont_show">Niy potrzebuja regularnyj synchrōnizacyje.*</string>
<string name="intro_autostart_title">Zgodność %s</string>
<string name="intro_autostart_text">Ta maszina może blokować synchrōnizacyjo. Jeźli cie to tyko, możesz to rozwiōnzać ino ryncznie.</string>
<string name="intro_autostart_dont_show">Już mōm wymogane sztelōnki. Niy spōminej mi wiyncyj.*</string>
<string name="intro_leave_unchecked">* Ôstow niyzaznaczōne, żeby spōmniało ô sobie niyskorzij. Idzie to zmiynić we sztelōnkach aplikacyje / %s</string>
<string name="intro_more_info">Wiyncyj informacyji</string>
<string name="intro_tasks_title">Sparcie Zadań</string>
<string name="intro_tasks_not_installed">Niyzainstalowane</string>
<string name="intro_tasks_opentasks_installed">OpenTasks zainstalowany</string>
<string name="intro_tasks_no_app_store">Niy je dostympny żodyn sklep ze aplikacyjami</string>
<string name="intro_tasks_text1"><![CDATA[Android niy mo wbudowanego sparcio zadań. Jeźli zadania sōm spiyrane ôd twojigo serwera, to %s może je synchrōnizować bez <a href="https://opentasks.app/">OpenTasks</a>.]]></string>
<string name="intro_tasks_text2">Do szyrszych działań jak podzadania abo zadania, co sie powtorzajōm, potrzebujesz ekstra aplikacyji.</string>
<string name="intro_tasks_dont_show">Niy potrzebuja sparcio zadań.*</string>
<string name="intro_open_source_title">Ôprogramowanie Open-Source</string>
<string name="intro_open_source_text">Sōm my radzi, że używosz %s, ôprogramowanie open-source. Tworzynie, utrzimanie i sparcie to ciynżko robota. Pōmyśl nad pōmocōm (sōm rozmajte spusoby) abo dowkōm. Fest by nos to ucieszyło!</string>
<string name="intro_open_source_details">Jak pōmōc/dociepnōńć sie</string>
<string name="intro_open_source_dont_show">Niy pokazuj zaś</string>
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">Przekłady</string>
<string name="about_libraries">Bibliotyki</string>
<string name="about_version">Wersyjo %1$s (%2$d)</string>
<string name="about_build_date">Skōmpilowano %s</string>
<string name="about_flavor_info">Ta wersyjo je do dystrybucyje ino na Google Play.</string>
<string name="about_license_info_no_warranty">Tyn program przichodzi BEZ ŻODNYJ GWARANCYJE. To je wolne ôprogramowanie i możesz je rozkludzać pod ôkryślōnymi warōnkami.</string>
<string name="about_translations_thanks"><![CDATA[<i>Dziynki: </i> %s]]></string>
<!--global settings-->
<string name="logging_couldnt_create_file">Niy szło stworzić zbioru dziynnika</string>
<string name="logging_notification_title">Dziynnik DAVx⁵</string>
@@ -34,6 +60,8 @@
<string name="navigation_drawer_subtitle">Adapter synchrōnizacyje CalDAV/CardDAV</string>
<string name="navigation_drawer_about">Ô DAVx⁵ / Licyncyjo</string>
<string name="navigation_drawer_beta_feedback">Przekoż ôpinijo</string>
<string name="install_email_client">Zainstaluj klijynt emaila</string>
<string name="install_browser">Zainstaluj przeglōndarka internetowo</string>
<string name="navigation_drawer_settings">Sztelōnki</string>
<string name="navigation_drawer_news_updates">Nowości i aktualizacyje</string>
<string name="navigation_drawer_external_links">Zewnyntrzne linki</string>
@@ -42,6 +70,8 @@
<string name="navigation_drawer_faq">Pytania i ôdpowiedzi</string>
<string name="navigation_drawer_forums">Pōmoc / Forum</string>
<string name="navigation_drawer_donate">Dowka</string>
<string name="navigation_drawer_privacy_policy">Polityka prywatności</string>
<string name="account_list_no_internet">Brak połōnczynio z internetym. Android niy bydzie synchrōnizować.</string>
<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>
@@ -127,6 +157,8 @@
<string name="login_account_name_required">Wymogane miano kōnta</string>
<string name="login_account_name_already_taken">Miano kōnta je już zajynte</string>
<string name="login_account_not_created">Kōnto niy mogło być stworzōne</string>
<string name="login_webview_tlserror">Problym bezpieczyństwa połōnczynio (kod felera %d)</string>
<string name="login_webview_retry">Sprōbuj zaś</string>
<string name="login_configuration_detection">Wykrywanie kōnfiguracyje</string>
<string name="login_querying_server">Czekej, ôdpytowanie serwera…</string>
<string name="login_no_caldav_carddav">Niy idzie znojść usugi CalDAV abo CardDAV.</string>
@@ -174,6 +206,14 @@
<item quantity="other">Zdarzynia starsze aniżeli %d dni bydōm zignorowane.</item>
</plurals>
<string name="settings_sync_time_range_past_message">Zdarzynia, co sōm starsze aniżeli podano wielość dni, bydōm zignorowane (może być 0). Ôstow prōzne, coby synchrōnizować wszyjske zdarzynia.</string>
<string name="settings_default_alarm">Wychodne spōmniynie</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">Wychodne spōmniynie minuta przed zdarzyniym</item>
<item quantity="few">Wychodne spōmniynie %d minuty przed zdarzyniym</item>
<item quantity="other">Wychodne spōmniynie %d minut przed zdarzyniym</item>
</plurals>
<string name="settings_default_alarm_off">Niy były stworzōne żodne wychodne spōmniynia</string>
<string name="settings_default_alarm_message">Jeźli wychodne spōmniynia bydōm tworzōne do zdarzyń bez spōmniynio: liczba minut przed zdarzyniym. Ôstow prōzne, żeby zastawić wychodne spōmniynia.</string>
<string name="settings_manage_calendar_colors">Zarzōndzej farbami kalyndorza</string>
<string name="settings_manage_calendar_colors_on">Farby kalyndorza sōm zarzōndzane ôd DAVx⁵</string>
<string name="settings_manage_calendar_colors_off">Farby kalyndorza niy sōm sztelowane ôd DAVx⁵</string>
@@ -182,6 +222,10 @@
<string name="settings_event_colors_off">Niy synchrōnizuj farbōw zdarzyń</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Spusōb grupy kōntaktōw</string>
<string-array name="settings_contact_group_method_entries">
<item>Grupy to sōm ôddzielne VCards</item>
<item>Grupy to sōm kategoryje co kōntakt</item>
</string-array>
<string name="settings_contact_group_method_change">Zmiyń spusōb grupowy</string>
<!--collection management-->
<string name="create_addressbook">Stwōrz ksiōnżka adresowo</string>

View File

@@ -6,6 +6,7 @@
<string name="manage_accounts">Hesapları yönet</string>
<string name="send">Gönder</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_license_info_no_warranty">Bu uygulama HİÇ BİR GARANTİ ile gelmemektedir. Bedava bir yazılımdır ve belli koşullar altında dağıtabilirsiniz.</string>
<!--global settings-->

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">Помилка мережі та вводу/виводу</string>
<string name="notification_channel_sync_io_errors_desc">Спливання часу відклику, проблеми зв\'язку, і т.п. (часто тимчасові)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">Бібліотеки</string>
<string name="about_version">Версія %1$s (%2$d)</string>

View File

@@ -28,6 +28,7 @@
<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>
<string name="intro_tasks_title">任务支持</string>
<string name="intro_tasks_not_installed">未安装</string>
<string name="intro_tasks_opentasks_installed">OpenTasks 已安装</string>
@@ -39,7 +40,7 @@
<string name="intro_open_source_text">我们很高兴您使用%s开源软件。开发维护和支持是艰苦的工作。请考虑捐款有很多方式或捐款。将不胜感激</string>
<string name="intro_open_source_details">如何捐款/捐款</string>
<string name="intro_open_source_dont_show">下次不再显示</string>
<string name="intro_more_info">更多信息</string>
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_translations">翻译</string>
<string name="about_libraries">程序库</string>
@@ -156,6 +157,8 @@
<string name="login_account_name_required">请输入账户名</string>
<string name="login_account_name_already_taken">账户名已被占用</string>
<string name="login_account_not_created">账户无法创建</string>
<string name="login_webview_tlserror">连接安全问题 (错误码%d)</string>
<string name="login_webview_retry">重试</string>
<string name="login_configuration_detection">正在配置</string>
<string name="login_querying_server">正在与服务器通信,请稍等…</string>
<string name="login_no_caldav_carddav">找不到 CalDAV 或 CardDAV 服务。</string>

View File

@@ -17,6 +17,7 @@
<string name="notification_channel_sync_io_errors">網際網絡和輸入輸出錯誤</string>
<string name="notification_channel_sync_io_errors_desc">超時,連接問題等(臨時問題)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries"></string>
<string name="about_version">版本號%1$s%2$d</string>

View File

@@ -43,6 +43,7 @@
<string name="intro_autostart_text">This device probably blocks synchronization. If you\'re affected, you can only resolve this manually.</string>
<string name="intro_autostart_dont_show">I have done the required settings. Don\'t remind me anymore.*</string>
<string name="intro_leave_unchecked">* Leave unchecked to be reminded later. Can be reset in app settings / %s.</string>
<string name="intro_more_info">More information</string>
<string name="intro_tasks_title">Tasks support</string>
<string name="intro_tasks_not_installed">Not installed</string>
<string name="intro_tasks_opentasks_installed">OpenTasks installed</string>
@@ -54,7 +55,25 @@
<string name="intro_open_source_text">We\'re happy that you use %s, which is open-source software. Development, maintenance and support are hard work. Please consider contributing (there are many ways) or a donation. It would be highly appreciated!</string>
<string name="intro_open_source_details">How to contribute/donate</string>
<string name="intro_open_source_dont_show">Don\'t show in the next time</string>
<string name="intro_more_info">More information</string>
<!-- PermissionsActivity -->
<string name="permissions_title">Permissions</string>
<string name="permissions_text">%s requires permissions to work properly.</string>
<string name="permissions_all_title">All of the below</string>
<string name="permissions_all_status_off">Use this to enable all features (recommended)</string>
<string name="permissions_all_status_on">All permissions granted</string>
<string name="permissions_contacts_title">Contacts permissions</string>
<string name="permissions_contacts_status_off">No contact sync (not recommended)</string>
<string name="permissions_contacts_status_on">Contact sync possible</string>
<string name="permissions_calendar_title">Calendar permissions</string>
<string name="permissions_calendar_status_off">No calendar sync (not recommended)</string>
<string name="permissions_calendar_status_on">Calendar sync possible</string>
<string name="permissions_tasks_title">OpenTasks permissions</string>
<string name="permissions_tasks_status_not_installed">No task sync (not installed)</string>
<string name="permissions_tasks_status_off">No task sync (not recommended)</string>
<string name="permissions_tasks_status_on">Task sync possible</string>
<string name="permissions_app_settings_hint">If a switch doesn\'t work, use app settings / Permissions.</string>
<string name="permissions_app_settings">App settings</string>
<!-- AboutActivity -->
<string name="about_translations">Translations</string>
@@ -130,6 +149,12 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_carddav_missing_permissions">No contacts sync (missing permissions)</string>
<string name="account_caldav_missing_calendar_permissions">No calendar sync (missing permissions)</string>
<string name="account_caldav_missing_tasks_permissions">No tasks sync (missing permissions)</string>
<string name="account_caldav_missing_permissions">No calendar and tasks sync (missing permissions)</string>
<string name="account_webcal_missing_calendar_permissions">Can\'t access calendars (missing permissions)</string>
<string name="account_permissions_action">Permissions</string>
<string name="account_no_address_books">There are no address books (yet).</string>
<string name="account_no_calendars">There are no calendars (yet).</string>
<string name="account_no_webcals">There are no calendar subscriptions (yet).</string>

View File

@@ -9,6 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="@string/address_books_authority"
android:isAlwaysSyncable="true"
android:allowParallelSyncs="true"
android:supportsUploading="false" />

View File

@@ -9,5 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="com.android.calendar"
android:supportsUploading="true"
android:isAlwaysSyncable="true" />
android:allowParallelSyncs="true"
android:supportsUploading="true" />

View File

@@ -9,5 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_address_book"
android:contentAuthority="com.android.contacts"
android:supportsUploading="true"
android:isAlwaysSyncable="true" />
android:allowParallelSyncs="true"
android:supportsUploading="true" />

View File

@@ -9,4 +9,5 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="org.dmfs.tasks"
android:allowParallelSyncs="true"
android:supportsUploading="true" />

View File

@@ -1,4 +1,5 @@
at.bitfire.davdroid.ui.intro.WelcomeFragment$Factory
at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment$Factory
at.bitfire.davdroid.ui.intro.OpenTasksFragment$Factory
at.bitfire.davdroid.ui.intro.PermissionsFragmentFactory
at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment$Factory
at.bitfire.davdroid.ui.intro.OpenSourceFragment$Factory

View File

@@ -10,10 +10,10 @@ buildscript {
ext.versions = [
aboutLibraries: '8.1.2',
appIntro: '5.1.0',
dav4jvm: '2.0',
dav4jvm: 'd0422e78',
dokka: '0.10.0',
kotlin: '1.3.71',
okhttp: '4.5.0'
kotlin: '1.3.72',
okhttp: '4.6.0'
]
repositories {
@@ -21,7 +21,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.0.0-beta05'
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${versions.aboutLibraries}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"

View File

@@ -0,0 +1 @@
CalDAV/CardDAV sinkronizazio eta bezeroa