Merge branch 'master' into patch-1

pull/129/head
Allan Nordhøy 4 years ago committed by GitHub
commit f894c3eb7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,9 +7,9 @@
*/
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@ -21,8 +21,8 @@ android {
minSdkVersion 19
targetSdkVersion 29
versionCode 114
versionName "1.16.2"
versionCode 200
versionName "2.0.0"
buildConfigField "boolean", "customCerts", "true"
}
@ -134,6 +134,8 @@ dependencies {
implementation "org.jetbrains.anko:anko-commons:0.10.4"
implementation "com.etesync:journalmanager:1.1.1"
def etebaseVersion = '0.2.0'
implementation "com.etebase:client:$etebaseVersion"
def acraVersion = '5.3.0'
implementation "ch.acra:acra-mail:$acraVersion"
@ -141,9 +143,16 @@ dependencies {
def supportVersion = '1.0.0'
implementation "androidx.legacy:legacy-support-core-ui:$supportVersion"
implementation "androidx.core:core:$supportVersion"
implementation "androidx.fragment:fragment:$supportVersion"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
// KTX extensions
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation 'com.google.android.material:material:1.2.0-beta01'
implementation "androidx.legacy:legacy-preference-v14:$supportVersion"
implementation 'com.github.yukuku:ambilwarna:2.0.1'

@ -127,7 +127,7 @@
<!-- Address book account -->
<service
android:name=".syncadapter.NullAuthenticatorService"
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
@ -233,6 +233,14 @@
android:exported="false"
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity
android:name=".ui.etebase.CollectionActivity"
android:exported="false"
/>
<activity
android:name=".ui.etebase.InvitationsActivity"
android:exported="false"
/>
<activity
android:name=".ui.ViewCollectionActivity"
android:exported="false"

@ -36,8 +36,12 @@ constructor(internal val context: Context, internal val account: Account) {
var uri: URI?
get() {
val uri = accountManager.getUserData(account, KEY_URI)
if (uri == null) {
return null
}
try {
return URI(accountManager.getUserData(account, KEY_URI))
return URI(uri)
} catch (e: URISyntaxException) {
return null
}
@ -73,6 +77,12 @@ constructor(internal val context: Context, internal val account: Account) {
get() = accountManager.getUserData(account, KEY_WIFI_ONLY_SSID)
set(ssid) = accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid)
var etebaseSession: String?
get() = accountManager.getUserData(account, KEY_ETEBASE_SESSION)
set(value) = accountManager.setUserData(account, KEY_ETEBASE_SESSION, value)
val isLegacy: Boolean
get() = authToken != null
// CalDAV settings
@ -216,6 +226,7 @@ constructor(internal val context: Context, internal val account: Account) {
private val KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key"
private val KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key"
private val KEY_WIFI_ONLY = "wifi_only"
private val KEY_ETEBASE_SESSION = "etebase_session"
// sync on WiFi only (default: false)
private val KEY_WIFI_ONLY_SSID = "wifi_only_ssid" // restrict sync to specific WiFi SSID
@ -243,10 +254,10 @@ constructor(internal val context: Context, internal val account: Account) {
val SYNC_INTERVAL_MANUALLY: Long = -1
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
fun setUserData(accountManager: AccountManager, account: Account, uri: URI, userName: String) {
fun setUserData(accountManager: AccountManager, account: Account, uri: URI?, userName: String) {
accountManager.setUserData(account, KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
accountManager.setUserData(account, KEY_USERNAME, userName)
accountManager.setUserData(account, KEY_URI, uri.toString())
accountManager.setUserData(account, KEY_URI, uri?.toString())
}
}
}

@ -158,22 +158,6 @@ class App : Application() {
private fun update(fromVersion: Int) {
Logger.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE)
if (fromVersion < 6) {
val data = this.data
val dbHelper = ServiceDB.OpenHelper(this)
val collections = readCollections(dbHelper)
for (info in collections) {
val journalEntity = JournalEntity(data, info)
data.insert(journalEntity)
}
val db = dbHelper.writableDatabase
db.delete(ServiceDB.Collections._TABLE, null, null)
db.close()
}
if (fromVersion < 7) {
/* Fix all of the etags to be non-null */
val am = AccountManager.get(this)
@ -234,21 +218,6 @@ class App : Application() {
}
private fun readCollections(dbHelper: ServiceDB.OpenHelper): List<CollectionInfo> {
val db = dbHelper.writableDatabase
val collections = LinkedList<CollectionInfo>()
val cursor = db.query(ServiceDB.Collections._TABLE, null, null, null, null, null, null)
while (cursor.moveToNext()) {
val values = ContentValues()
DatabaseUtils.cursorRowToContentValues(cursor, values)
collections.add(CollectionInfo.fromDB(values))
}
db.close()
cursor.close()
return collections
}
fun migrateServices(dbHelper: ServiceDB.OpenHelper) {
val db = dbHelper.readableDatabase
val data = this.data

@ -32,9 +32,10 @@ public class Constants {
public static final Uri dashboard = webUri.buildUpon().appendEncodedPath("dashboard/").build();
public static final Uri faqUri = webUri.buildUpon().appendEncodedPath("faq/").build();
public static final Uri helpUri = webUri.buildUpon().appendEncodedPath("user-guide/android/").build();
public static final Uri forgotPassword = webUri.buildUpon().appendEncodedPath("accounts/password/reset/").build();
public static final Uri forgotPassword = faqUri.buildUpon().fragment("forgot-password").build();
public static final Uri serviceUrl = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://api.etesync.com/" : DEBUG_REMOTE_URL);
public static final String etebaseServiceUrl = (DEBUG_REMOTE_URL == null) ? "https://api.etebase.com/partner/etesync/" : DEBUG_REMOTE_URL;
public static final String PRODID_BASE = "-//EteSync//" + BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_NAME;
@ -43,4 +44,8 @@ public class Constants {
public final static String KEY_ACCOUNT = "account",
KEY_COLLECTION_INFO = "collectionInfo";
public final static String ETEBASE_TYPE_ADDRESS_BOOK = "etebase.vcard";
public final static String ETEBASE_TYPE_CALENDAR = "etebase.vevent";
public final static String ETEBASE_TYPE_TASKS = "etebase.vtodo";
}

@ -0,0 +1,166 @@
package com.etesync.syncadapter
import android.content.Context
import com.etebase.client.*
import com.etebase.client.Collection
import okhttp3.OkHttpClient
import java.io.File
import java.util.*
/*
File structure:
cache_dir/
user1/ <--- the name of the user
stoken <-- the stokens of the collection fetch
cols/
UID1/ - The uid of the first col
...
UID2/ - The uid of the second col
col <-- the col itself
stoken <-- the stoken of the items fetch
items/
item_uid1 <-- the item with uid 1
item_uid2
...
*/
class EtebaseLocalCache private constructor(context: Context, username: String) {
private val filesDir: File = File(context.filesDir, username)
private val colsDir: File
init {
colsDir = File(filesDir, "cols")
colsDir.mkdirs()
}
private fun getCollectionItemsDir(colUid: String): File {
val colsDir = File(filesDir, "cols")
val colDir = File(colsDir, colUid)
return File(colDir, "items")
}
fun clearUserCache() {
filesDir.deleteRecursively()
}
fun saveStoken(stoken: String) {
val stokenFile = File(filesDir, "stoken")
stokenFile.writeText(stoken)
}
fun loadStoken(): String? {
val stokenFile = File(filesDir, "stoken")
return if (stokenFile.exists()) stokenFile.readText() else null
}
fun collectionSaveStoken(colUid: String, stoken: String) {
val colDir = File(colsDir, colUid)
val stokenFile = File(colDir, "stoken")
stokenFile.writeText(stoken)
}
fun collectionLoadStoken(colUid: String): String? {
val colDir = File(colsDir, colUid)
val stokenFile = File(colDir, "stoken")
return if (stokenFile.exists()) stokenFile.readText() else null
}
fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List<CachedCollection> {
return colsDir.list().map {
val colDir = File(colsDir, it)
val colFile = File(colDir, "col")
val content = colFile.readBytes()
colMgr.cacheLoad(content)
}.filter { withDeleted || !it.isDeleted }.map{
CachedCollection(it, it.meta)
}
}
fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection? {
val colDir = File(colsDir, colUid)
val colFile = File(colDir, "col")
if (!colFile.exists()) {
return null
}
val content = colFile.readBytes()
return colMgr.cacheLoad(content).let {
CachedCollection(it, it.meta)
}
}
fun collectionSet(colMgr: CollectionManager, collection: Collection) {
val colDir = File(colsDir, collection.uid)
colDir.mkdirs()
val colFile = File(colDir, "col")
colFile.writeBytes(colMgr.cacheSaveWithContent(collection))
val itemsDir = getCollectionItemsDir(collection.uid)
itemsDir.mkdirs()
}
fun collectionUnset(colMgr: CollectionManager, colUid: String) {
val colDir = File(colsDir, colUid)
colDir.deleteRecursively()
}
fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List<CachedItem> {
val itemsDir = getCollectionItemsDir(colUid)
return itemsDir.list().map {
val itemFile = File(itemsDir, it)
val content = itemFile.readBytes()
itemMgr.cacheLoad(content)
}.filter { withDeleted || !it.isDeleted }.map {
CachedItem(it, it.meta, it.contentString)
}
}
fun itemGet(itemMgr: ItemManager, colUid: String, itemUid: String): CachedItem? {
val itemsDir = getCollectionItemsDir(colUid)
val itemFile = File(itemsDir, itemUid)
if (!itemFile.exists()) {
return null
}
val content = itemFile.readBytes()
return itemMgr.cacheLoad(content).let {
CachedItem(it, it.meta, it.contentString)
}
}
fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) {
val itemsDir = getCollectionItemsDir(colUid)
val itemFile = File(itemsDir, item.uid)
itemFile.writeBytes(itemMgr.cacheSaveWithContent(item))
}
fun itemUnset(itemMgr: ItemManager, colUid: String, itemUid: String) {
val itemsDir = getCollectionItemsDir(colUid)
val itemFile = File(itemsDir, itemUid)
itemFile.delete()
}
companion object {
private val localCacheCache: HashMap<String, EtebaseLocalCache> = HashMap()
fun getInstance(context: Context, username: String): EtebaseLocalCache {
synchronized(localCacheCache) {
val cached = localCacheCache.get(username)
if (cached != null) {
return cached
} else {
val ret = EtebaseLocalCache(context, username)
localCacheCache.set(username, ret)
return ret
}
}
}
// FIXME: If we ever cache this we need to cache bust on changePassword
fun getEtebase(context: Context, httpClient: OkHttpClient, settings: AccountSettings): Account {
val client = Client.create(httpClient, settings.uri?.toString())
return Account.restore(client, settings.etebaseSession!!, null)
}
}
}
data class CachedCollection(val col: Collection, val meta: CollectionMetadata)
data class CachedItem(val item: Item, val meta: ItemMetadata, val content: String)

@ -68,7 +68,6 @@ class HttpClient private constructor(
) {
private var certManager: CustomCertManager? = null
private var certificateAlias: String? = null
private var cache: Cache? = null
private val orig = sharedClient.newBuilder()

@ -48,23 +48,6 @@ class CollectionInfo : com.etesync.journalmanager.model.CollectionInfo() {
return info
}
fun fromDB(values: ContentValues): CollectionInfo {
val info = CollectionInfo()
info.id = values.getAsLong(Collections.ID)!!
info.serviceID = values.getAsInteger(Collections.SERVICE_ID)!!
info.uid = values.getAsString(Collections.URL)
info.displayName = values.getAsString(Collections.DISPLAY_NAME)
info.description = values.getAsString(Collections.DESCRIPTION)
info.color = values.getAsInteger(Collections.COLOR)
info.timeZone = values.getAsString(Collections.TIME_ZONE)
info.selected = values.getAsInteger(Collections.SYNC) != 0
return info
}
fun fromJson(json: String): CollectionInfo {
return GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, CollectionInfo::class.java)
}

@ -19,7 +19,9 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import at.bitfire.vcard4android.*
import com.etebase.client.CollectionAccessLevel
import com.etesync.syncadapter.App
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -70,6 +72,37 @@ class LocalAddressBook(
return addressBook
}
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, cachedCollection: CachedCollection): LocalAddressBook {
val col = cachedCollection.col
val accountManager = AccountManager.get(context)
val account = Account(accountName(mainAccount, cachedCollection), App.addressBookAccountType)
val userData = initialUserData(mainAccount, col.uid)
Logger.log.log(Level.INFO, "Creating local address book $account", userData)
if (!accountManager.addAccountExplicitly(account, null, userData))
throw IllegalStateException("Couldn't create address book account")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// Android < 7 seems to lose the initial user data sometimes, so set it a second time
// https://forums.bitfire.at/post/11644
userData.keySet().forEach { key ->
accountManager.setUserData(account, key, userData.getString(key))
}
}
val addressBook = LocalAddressBook(context, account, provider)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.settings = values
addressBook.readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly
return addressBook
}
fun find(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context)
.getAccountsByType(App.addressBookAccountType)
@ -103,6 +136,13 @@ class LocalAddressBook(
return sb.toString()
}
fun accountName(mainAccount: Account, cachedCollection: CachedCollection): String {
val col = cachedCollection.col
val meta = cachedCollection.meta
val displayName = meta.name
return "${displayName} (${mainAccount.name} ${col.uid.substring(0, 2)})"
}
fun initialUserData(mainAccount: Account, url: String): Bundle {
val bundle = Bundle(3)
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
@ -181,6 +221,37 @@ class LocalAddressBook(
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
fun update(cachedCollection: CachedCollection) {
val col = cachedCollection.col
val newAccountName = accountName(mainAccount, cachedCollection)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
val accountManager = AccountManager.get(context)
val future = accountManager.renameAccount(account, newAccountName, {
try {
// update raw contacts to new account name
if (provider != null) {
val values = ContentValues(1)
values.put(RawContacts.ACCOUNT_NAME, newAccountName)
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
arrayOf(account.name, account.type))
}
} catch (e: RemoteException) {
Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e)
}
}, null)
account = future.result
}
readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly
Logger.log.info("Address book write permission? = ${!readOnly}")
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
fun delete() {
val accountManager = AccountManager.get(context)
@ -279,6 +350,16 @@ class LocalAddressBook(
}
}
override fun findByFilename(filename: String): LocalAddress? {
val found = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull()
if (found != null) {
return found
} else {
return queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull()
}
}
fun findGroupById(id: Long): LocalGroup =
queryGroups("${Groups._ID}=?", arrayOf(id.toString())).firstOrNull()
?: throw FileNotFoundException()

@ -18,9 +18,13 @@ import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import at.bitfire.ical4android.*
import com.etebase.client.CollectionAccessLevel
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.resource.LocalEvent.Companion.COLUMN_UID
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
import java.util.*
import java.util.logging.Level
@ -31,7 +35,21 @@ class LocalCalendar private constructor(
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
companion object {
val defaultColor = -0x743cb6 // light green 500
val defaultColor = -0x743cb6 // light green 500 - should be "8BC349"?
fun parseColor(color_: String?): Int {
if (color_.isNullOrBlank()) {
return defaultColor
}
val color = color_.replaceFirst("^#".toRegex(), "")
if (color.length == 8) {
return (color.substring(0, 6).toLong(16) or (color.substring(6, 8).toLong(16) shl 24)).toInt()
} else if (color.length == 6) {
return (color.toLong(16) or (0xFF000000)).toInt()
} else {
return defaultColor
}
}
val COLUMN_CTAG = Calendars.CAL_SYNC1
@ -50,6 +68,21 @@ class LocalCalendar private constructor(
return AndroidCalendar.create(account, provider, values)
}
fun create(account: Account, provider: ContentProviderClient, cachedCollection: CachedCollection): Uri {
val values = valuesFromCachedCollection(cachedCollection, true)
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
return AndroidCalendar.create(account, provider, values)
}
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
@ -85,6 +118,30 @@ class LocalCalendar private constructor(
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
return values
}
private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues {
val values = ContentValues()
val col = cachedCollection.col
val meta = cachedCollection.meta
values.put(Calendars.NAME, col.uid)
values.put(Calendars.CALENDAR_DISPLAY_NAME, meta.name)
if (withColor)
values.put(Calendars.CALENDAR_COLOR, parseColor(meta.color))
if (col.accessLevel == CollectionAccessLevel.ReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
else {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
return values
}
}
override val url: String?
@ -93,6 +150,9 @@ class LocalCalendar private constructor(
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
update(valuesFromCollectionInfo(journalEntity, updateColor))
fun update(cachedCollection: CachedCollection, updateColor: Boolean) =
update(valuesFromCachedCollection(cachedCollection, updateColor))
override fun findDeleted() =
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
@ -122,7 +182,10 @@ class LocalCalendar private constructor(
= queryEvents(null, null)
override fun findByUid(uid: String): LocalEvent?
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
= queryEvents(COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull()
override fun findByFilename(filename: String): LocalEvent?
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull()
fun processDirtyExceptions() {
// process deleted exceptions

@ -17,6 +17,7 @@ interface LocalCollection<out T: LocalResource<*>> {
fun findAll(): List<T>
fun findByUid(uid: String): T?
fun findByFilename(filename: String): T?
fun count(): Long

@ -52,7 +52,7 @@ class LocalContact : AndroidContact, LocalAddress {
override// The same now
val uuid: String?
get() = fileName
get() = contact?.uid
override val isLocalOnly: Boolean
get() = TextUtils.isEmpty(eTag)
@ -88,9 +88,11 @@ class LocalContact : AndroidContact, LocalAddress {
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
}
override fun clearDirty(eTag: String) {
override fun clearDirty(eTag: String?) {
val values = ContentValues(3)
values.put(AndroidContact.COLUMN_ETAG, eTag)
if (eTag != null) {
values.put(AndroidContact.COLUMN_ETAG, eTag)
}
values.put(ContactsContract.RawContacts.DIRTY, 0)
if (LocalContact.HASH_HACK) {
@ -105,15 +107,26 @@ class LocalContact : AndroidContact, LocalAddress {
this.eTag = eTag
}
override fun prepareForUpload() {
override fun legacyPrepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString()
val values = ContentValues(2)
values.put(AndroidContact.COLUMN_FILENAME, uid)
val fileName = fileName_ ?: uid
values.put(AndroidContact.COLUMN_FILENAME, fileName)
values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
this.fileName = fileName
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(AndroidContact.COLUMN_FILENAME, fileName)
values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
fileName = uid
contact?.uid = uid
this.fileName = fileName
}
override fun populateData(mimeType: String, row: ContentValues) {

@ -38,14 +38,14 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
private var saveAsDirty = false // When true, the resource will be saved as dirty
private var fileName: String? = null
override var fileName: String? = null
var eTag: String? = null
var weAreOrganizer = true
override val content: String
get() {
Logger.log.log(Level.FINE, "Preparing upload of event " + fileName!!, event)
Logger.log.log(Level.FINE, "Preparing upload of event $fileName} ${event}")
val os = ByteArrayOutputStream()
event?.write(os)
@ -58,7 +58,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
override// Now the same
val uuid: String?
get() = fileName
get() = event?.uid
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) {
this.fileName = fileName
@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
/* custom queries */
override fun prepareForUpload() {
override fun legacyPrepareForUpload(fileName_: String?) {
var uid: String? = null
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
if (c.moveToNext())
@ -142,30 +142,42 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
uid = UUID.randomUUID().toString()
c.close()
val newFileName = uid
val fileName = fileName_ ?: uid
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events._SYNC_ID, fileName)
values.put(COLUMN_UID, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName
this.fileName = fileName
val event = this.event
if (event != null)
event.uid = uid
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(Events._SYNC_ID, fileName)
values.put(COLUMN_UID, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
event?.uid = uid
this.fileName = fileName
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(CalendarContract.Events.DELETED, 0)
calendar.provider.update(eventSyncURI(), values, null, null)
}
override fun clearDirty(eTag: String) {
override fun clearDirty(eTag: String?) {
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
if (eTag != null) {
values.put(COLUMN_ETAG, eTag)
}
if (event != null)
values.put(COLUMN_SEQUENCE, event?.sequence)
calendar.provider.update(eventSyncURI(), values, null, null)

@ -120,13 +120,15 @@ class LocalGroup : AndroidGroup, LocalAddress {
return values
}
override fun clearDirty(eTag: String) {
override fun clearDirty(eTag: String?) {
val id = requireNotNull(id)
val values = ContentValues(2)
values.put(Groups.DIRTY, 0)
this.eTag = eTag
values.put(AndroidGroup.COLUMN_ETAG, eTag)
if (eTag != null) {
values.put(AndroidGroup.COLUMN_ETAG, eTag)
}
update(values)
// update cached group memberships
@ -154,15 +156,26 @@ class LocalGroup : AndroidGroup, LocalAddress {
batch.commit()
}
override fun prepareForUpload() {
override fun legacyPrepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString()
val values = ContentValues(2)
values.put(AndroidGroup.COLUMN_FILENAME, uid)
val fileName = fileName_ ?: uid
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
values.put(AndroidGroup.COLUMN_UID, uid)
update(values)
fileName = uid
this.fileName = fileName
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
values.put(AndroidGroup.COLUMN_UID, uid)
addressBook.provider?.update(groupSyncUri(), values, null, null)
contact?.uid = uid
this.fileName = fileName
}
override fun resetDeleted() {

@ -10,6 +10,7 @@ package com.etesync.syncadapter.resource
interface LocalResource<in TData: Any> {
val uuid: String?
val fileName: String?
/** True if doesn't exist on server yet, false otherwise. */
val isLocalOnly: Boolean
@ -19,9 +20,11 @@ interface LocalResource<in TData: Any> {
fun delete(): Int
fun prepareForUpload()
fun legacyPrepareForUpload(fileName: String?)
fun clearDirty(eTag: String)
fun prepareForUpload(fileName: String, uid: String)
fun clearDirty(eTag: String?)
fun resetDeleted()
}

@ -31,7 +31,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
private var saveAsDirty = false // When true, the resource will be saved as dirty
private var fileName: String? = null
override var fileName: String? = null
var eTag: String? = null
override val content: String
@ -49,7 +49,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
override// Now the same
val uuid: String?
get() = fileName
get() = task?.uid
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?)
: super(taskList, task) {
@ -96,7 +96,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
/* custom queries */
override fun prepareForUpload() {
override fun legacyPrepareForUpload(fileName_: String?) {
var uid: String? = null
val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null)
if (c.moveToNext())
@ -106,27 +106,40 @@ class LocalTask : AndroidTask, LocalResource<Task> {
c.close()
val fileName = fileName_ ?: uid
val values = ContentValues(2)
values.put(TaskContract.Tasks._SYNC_ID, uid)
values.put(TaskContract.Tasks._SYNC_ID, fileName)
values.put(COLUMN_UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = uid
this.fileName = fileName
val task = this.task
if (task != null)
task.uid = uid
}
override fun prepareForUpload(fileName: String, uid: String) {
val values = ContentValues(2)
values.put(TaskContract.Tasks._SYNC_ID, fileName)
values.put(COLUMN_UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
task?.uid = uid
this.fileName = fileName
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(TaskContract.Tasks._DELETED, 0)
taskList.provider.client.update(taskSyncURI(), values, null, null)
}
override fun clearDirty(eTag: String) {
override fun clearDirty(eTag: String?) {
val values = ContentValues(2)
values.put(TaskContract.Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
if (eTag != null) {
values.put(COLUMN_ETAG, eTag)
}
if (task != null)
values.put(COLUMN_SEQUENCE, task?.sequence)
taskList.provider.client.update(taskSyncURI(), values, null, null)

@ -19,6 +19,7 @@ import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.model.JournalEntity
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
@ -54,6 +55,14 @@ class LocalTaskList private constructor(
return create(account, provider, values)
}
fun create(account: Account, provider: TaskProvider, cachedCollection: CachedCollection): Uri {
val values = valuesFromCachedCollection(cachedCollection, true)
values.put(TaskLists.OWNER, account.name)
values.put(TaskLists.SYNC_ENABLED, 1)
values.put(TaskLists.VISIBLE, 1)
return create(account, provider, values)
}
fun findByName(account: Account, provider: TaskProvider, factory: Factory, name: String): LocalTaskList?
= AndroidTaskList.find(account, provider, factory, TaskLists._SYNC_ID + "==?", arrayOf(name)).firstOrNull()
@ -70,6 +79,18 @@ class LocalTaskList private constructor(
return values
}
private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues {
val col = cachedCollection.col
val meta = cachedCollection.meta
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, col.uid)
values.put(TaskLists.LIST_NAME, meta.name)
if (withColor)
values.put(TaskLists.LIST_COLOR, LocalCalendar.parseColor(meta.color))
return values
}
}
override val url: String?
@ -78,6 +99,9 @@ class LocalTaskList private constructor(
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
update(valuesFromCollectionInfo(journalEntity, updateColor))
fun update(cachedCollection: CachedCollection, updateColor: Boolean) =
update(valuesFromCachedCollection(cachedCollection, updateColor))
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
override fun findDirty(limit: Int?): List<LocalTask> {
@ -101,7 +125,10 @@ class LocalTaskList private constructor(
= queryTasks(Tasks._SYNC_ID + " IS NULL", null)
override fun findByUid(uid: String): LocalTask?
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
= queryTasks(LocalTask.COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull()
override fun findByFilename(filename: String): LocalTask?
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull()
override fun count(): Long {
try {

@ -15,10 +15,7 @@ import android.content.*
import android.os.Bundle
import android.provider.ContactsContract
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -36,9 +33,6 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
private class AddressBooksSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override val syncErrorTitle = R.string.sync_error_contacts
override val notificationManager = SyncNotification(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
if (contactsProvider == null) {
@ -53,7 +47,11 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run()
updateLocalAddressBooks(contactsProvider, account)
if (settings.isLegacy) {
legacyUpdateLocalAddressBooks(contactsProvider, account)
} else {
updateLocalAddressBooks(contactsProvider, account, settings)
}
contactsProvider.release()
@ -63,15 +61,57 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
val syncExtras = Bundle(extras)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
Logger.log.info("Address book sync complete")
}
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
val remote = HashMap<String, CachedCollection>()
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK }
}
for (collection in collections) {
remote[collection.col.uid] = collection
}
val local = LocalAddressBook.find(context, provider, account)
// delete obsolete local calendar
for (addressBook in local) {
val url = addressBook.url
val collection = remote[url]
if (collection == null) {
Logger.log.fine("Deleting obsolete local addressBook $url")
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
Logger.log.fine("Updating local addressBook $url")
addressBook.update(collection)
// we already have a local addressBook for this remote collection, don't take into consideration anymore
remote.remove(url)
}
}
// create new local calendars
for (url in remote.keys) {
val cachedCollection = remote[url]!!
Logger.log.info("Adding local calendar list $cachedCollection")
LocalAddressBook.create(context, provider, account, cachedCollection)
}
}
@Throws(ContactsStorageException::class, AuthenticatorException::class, OperationCanceledException::class, IOException::class)
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
private fun legacyUpdateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
val context = context
val data = (getContext().applicationContext as App).data
val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.ADDRESS_BOOK)

@ -17,6 +17,7 @@ import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.Item
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
@ -43,7 +44,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
get() = context.getString(R.string.sync_error_calendar, account.name)
override val syncSuccessfullyTitle: String
get() = context.getString(R.string.sync_successfully_calendar, info.displayName,
get() = context.getString(R.string.sync_successfully_calendar, localCalendar().displayName,
account.name)
init {
@ -59,7 +60,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
if (!super.prepare())
return false
journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!)
if (isLegacy) {
journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!)
}
return true
}
@ -77,6 +80,32 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
return localCollection as LocalCalendar
}
override fun processItem(item: Item) {
val local = localCollection!!.findByFilename(item.uid)
if (!item.isDeleted) {
val inputReader = StringReader(String(item.content))
val events = Event.eventsFromReader(inputReader)
if (events.size == 0) {
Logger.log.warning("Received VCard without data, ignoring")
return
} else if (events.size > 1) {
Logger.log.warning("Received multiple VCALs, using first one")
}
val event = events[0]
processEvent(item, event, local)
} else {
if (local != null) {
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
local.delete()
} else {
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
}
}
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
override fun processSyncEntryImpl(cEntry: SyncEntry) {
val inputReader = StringReader(cEntry.content)
@ -93,7 +122,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
val local = localCollection!!.findByUid(event.uid!!)
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processEvent(event, local)
legacyProcessEvent(event, local)
} else {
if (local != null) {
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
@ -105,8 +134,8 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
override fun createLocalEntries() {
super.createLocalEntries()
override fun prepareLocal() {
super.prepareLocal()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createInviteAttendeesNotification()
@ -128,7 +157,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
private fun createInviteAttendeesNotification(event: Event, icsContent: String) {
val intent = EventEmailInvitation(context, account).createIntent(event, icsContent)
if (intent != null) {
val notificationHelper = SyncNotification(context, event.uid!!, event.uid!!.hashCode())
val notificationHelper = SyncNotification(context, icsContent, event.hashCode())
notificationHelper.notify(
context.getString(
R.string.sync_calendar_attendees_notification_title, event.summary),
@ -138,8 +167,26 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
}
private fun processEvent(item: Item, newData: Event, _localEvent: LocalEvent?): LocalEvent {
var localEvent = _localEvent
// delete local event, if it exists
if (localEvent != null) {
Logger.log.info("Updating " + newData.uid + " in local calendar")
localEvent.eTag = item.etag
localEvent.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding " + newData.uid + " to local calendar")
localEvent = LocalEvent(localCalendar(), newData, item.uid, item.etag)
localEvent.add()
syncResult.stats.numInserts++
}
return localEvent
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
private fun processEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent {
private fun legacyProcessEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent {
var localEvent = _localEvent
// delete local event, if it exists
if (localEvent != null) {

@ -13,10 +13,7 @@ import android.os.Bundle
import android.provider.CalendarContract
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.CalendarStorageException
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -32,9 +29,6 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
private class SyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override val syncErrorTitle = R.string.sync_error_calendar
override val notificationManager = SyncNotification(context, "journals-calendar", Constants.NOTIFICATION_CALENDAR_SYNC)
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val settings = AccountSettings(context, account)
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
@ -42,7 +36,11 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
RefreshCollections(account, CollectionInfo.Type.CALENDAR).run()
updateLocalCalendars(provider, account, settings)
if (settings.isLegacy) {
legacyUpdateLocalCalendars(provider, account, settings)
} else {
updateLocalCalendars(provider, account, settings)
}
val principal = settings.uri?.toHttpUrlOrNull()!!
@ -56,8 +54,52 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
Logger.log.info("Calendar sync complete")
}
@Throws(CalendarStorageException::class)
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
val remote = HashMap<String, CachedCollection>()
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
}
for (collection in collections) {
remote[collection.col.uid] = collection
}
val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null)
val updateColors = settings.manageCalendarColors
// delete obsolete local calendar
for (calendar in local) {
val url = calendar.name
val collection = remote[url]
if (collection == null) {
Logger.log.fine("Deleting obsolete local calendar $url")
calendar.delete()
} else {
// remote CollectionInfo found for this local collection, update data
Logger.log.fine("Updating local calendar $url")
calendar.update(collection, updateColors)
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url)
}
}
// create new local calendars
for (url in remote.keys) {
val cachedCollection = remote[url]!!
Logger.log.info("Adding local calendar list $cachedCollection")
LocalCalendar.create(account, provider, cachedCollection)
}
}
@Throws(CalendarStorageException::class)
private fun legacyUpdateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
val data = (context.applicationContext as App).data
val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.CALENDAR)

@ -27,9 +27,6 @@ class ContactsSyncAdapterService : SyncAdapterService() {
private class ContactsSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override val syncErrorTitle = R.string.sync_error_contacts
override val notificationManager = SyncNotification(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val addressBook = LocalAddressBook(context, account, provider)

@ -19,6 +19,7 @@ import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.JournalEntryManager
import com.etesync.journalmanager.model.SyncEntry
import com.etebase.client.Item
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient
@ -77,7 +78,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
}
journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url)
if (isLegacy) {
journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url)
}
return true
}
@ -127,6 +130,32 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
return localCollection as LocalAddressBook
}
override fun processItem(item: Item) {
val local = localCollection!!.findByFilename(item.uid)
if (!item.isDeleted) {
val inputReader = StringReader(String(item.content))
val contacts = Contact.fromReader(inputReader, resourceDownloader)
if (contacts.size == 0) {
Logger.log.warning("Received VCard without data, ignoring")
return
} else if (contacts.size > 1) {
Logger.log.warning("Received multiple VCALs, using first one")
}
val contact = contacts[0]
processContact(item, contact, local)
} else {
if (local != null) {
Logger.log.info("Removing local record which has been deleted on the server")
local.delete()
} else {
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
}
}
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
override fun processSyncEntryImpl(cEntry: SyncEntry) {
val inputReader = StringReader(cEntry.content)
@ -142,7 +171,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
val local = localCollection!!.findByUid(contact.uid!!)
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processContact(contact, local)
legacyProcessContact(contact, local)
} else {
if (local != null) {
Logger.log.info("Removing local record which has been deleted on the server")
@ -153,8 +182,65 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
}
}
private fun processContact(item: Item, newData: Contact, _local: LocalAddress?): LocalAddress {
var local = _local
val uuid = newData.uid
// update local contact, if it exists
if (local != null) {
Logger.log.log(Level.INFO, "Updating $uuid in local address book")
if (local is LocalGroup && newData.group) {
// update group
val group: LocalGroup = local
group.eTag = item.etag
group.update(newData)
syncResult.stats.numUpdates++
} else if (local is LocalContact && !newData.group) {
// update contact
val contact: LocalContact = local
contact.eTag = item.etag
contact.update(newData)
syncResult.stats.numUpdates++
} else {
// group has become an individual contact or vice versa
try {
local.delete()
local = null
} catch (e: CalendarStorageException) {
// CalendarStorageException is not used by LocalGroup and LocalContact
}
}
}
if (local == null) {
if (newData.group) {
Logger.log.log(Level.INFO, "Creating local group", item.uid)
val group = LocalGroup(localAddressBook(), newData, item.uid, item.etag)
group.add()
local = group
} else {
Logger.log.log(Level.INFO, "Creating local contact", item.uid)
val contact = LocalContact(localAddressBook(), newData, item.uid, item.etag)
contact.add()
local = contact
}
syncResult.stats.numInserts++
}
if (LocalContact.HASH_HACK && local is LocalContact)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
local.updateHashCode(null)
return local
}
@Throws(IOException::class, ContactsStorageException::class)
private fun processContact(newData: Contact, _local: LocalAddress?): LocalAddress {
private fun legacyProcessContact(newData: Contact, _local: LocalAddress?): LocalAddress {
var local = _local
val uuid = newData.uid
// update local contact, if it exists

@ -22,11 +22,14 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.util.Pair
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.FetchOptions
import com.etebase.client.exceptions.ConnectionException
import com.etebase.client.exceptions.TemporaryServerErrorException
import com.etebase.client.exceptions.UnauthorizedException
import com.etesync.syncadapter.*
import com.etesync.journalmanager.Crypto
import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -96,8 +99,8 @@ abstract class SyncAdapterService : Service() {
}
abstract class SyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, false) {
abstract val syncErrorTitle: Int
abstract val notificationManager: SyncNotification
private val syncErrorTitle: Int = R.string.sync_error_generic
private val notificationManager = SyncNotification(context, "refresh-collections", Constants.NOTIFICATION_REFRESH_COLLECTIONS)
abstract fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult)
@ -117,6 +120,12 @@ abstract class SyncAdapterService : Service() {
} catch (e: Exceptions.ServiceUnavailableException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
} catch (e: TemporaryServerErrorException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
} catch (e: ConnectionException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
} catch (e: Exceptions.IgnorableHttpException) {
// Ignore
} catch (e: Exception) {
@ -132,7 +141,7 @@ abstract class SyncAdapterService : Service() {
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(Constants.KEY_ACCOUNT, account)
if (e !is Exceptions.UnauthorizedException) {
if (e !is Exceptions.UnauthorizedException && e !is UnauthorizedException) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
}
@ -208,30 +217,70 @@ abstract class SyncAdapterService : Service() {
val settings = AccountSettings(context, account)
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!)
var journals = journalFetcher.list(journalsManager, settings, serviceType)
if (journals.isEmpty()) {
journals = LinkedList()
try {
val info = CollectionInfo.defaultForServiceType(serviceType)
val uid = JournalManager.Journal.genUid()
info.uid = uid
val crypto = Crypto.CryptoManager(info.version, settings.password(), uid)
val journal = JournalManager.Journal(crypto, info.toJson(), uid)
journalsManager.create(journal)
journals.add(Pair(journal, info))
} catch (e: Exceptions.AssociateNotAllowedException) {
// Skip for now
if (settings.isLegacy) {
val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!)
var journals = journalFetcher.list(journalsManager, settings, serviceType)
if (journals.isEmpty()) {
journals = LinkedList()
try {
val info = CollectionInfo.defaultForServiceType(serviceType)
val uid = JournalManager.Journal.genUid()
info.uid = uid
val crypto = Crypto.CryptoManager(info.version, settings.password(), uid)
val journal = JournalManager.Journal(crypto, info.toJson(), uid)
journalsManager.create(journal)
journals.add(Pair(journal, info))
} catch (e: Exceptions.AssociateNotAllowedException) {
// Skip for now
}
}
legacySaveCollections(journals)
httpClient.close()
return
}
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
synchronized(etebaseLocalCache) {
val cacheAge = 5 * 1000 // 5 seconds - it's just a hack for burst fetching
val now = System.currentTimeMillis()
val lastCollectionsFetch = collectionLastFetchMap[account.name] ?: 0
if (abs(now - lastCollectionsFetch) <= cacheAge) {
return@synchronized
}
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
var stoken = etebaseLocalCache.loadStoken()
var done = false
while (!done) {
val colList = colMgr.list(FetchOptions().stoken(stoken))
for (col in colList.data) {
etebaseLocalCache.collectionSet(colMgr, col)
}
for (col in colList.removedMemberships) {
etebaseLocalCache.collectionUnset(colMgr, col.uid())
}
stoken = colList.stoken
done = colList.isDone
if (stoken != null) {
etebaseLocalCache.saveStoken(stoken)
}
}
collectionLastFetchMap[account.name] = now
}
saveCollections(journals)
httpClient.close()
}
private fun saveCollections(journals: Iterable<Pair<JournalManager.Journal, CollectionInfo>>) {
private fun legacySaveCollections(journals: Iterable<Pair<JournalManager.Journal, CollectionInfo>>) {
val data = (context.applicationContext as App).data
val service = JournalModel.Service.fetchOrCreate(data, account.name, serviceType)
@ -269,5 +318,6 @@ abstract class SyncAdapterService : Service() {
companion object {
val journalFetcher = CachedJournalFetcher()
var collectionLastFetchMap = HashMap<String, Long>()
}
}

@ -16,6 +16,11 @@ import android.os.Bundle
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.*
import com.etebase.client.exceptions.ConnectionException
import com.etebase.client.exceptions.HttpException
import com.etebase.client.exceptions.TemporaryServerErrorException
import com.etebase.client.exceptions.UnauthorizedException
import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.journalmanager.Crypto
@ -25,10 +30,13 @@ import com.etesync.journalmanager.model.SyncEntry
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.*
import com.etesync.journalmanager.model.SyncEntry.Actions.ADD
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R
import com.etesync.syncadapter.resource.*
import com.etesync.syncadapter.ui.AccountsActivity
import com.etesync.syncadapter.ui.DebugInfoActivity
import com.etesync.syncadapter.ui.ViewCollectionActivity
import com.etesync.syncadapter.ui.etebase.CollectionActivity
import org.jetbrains.anko.defaultSharedPreferences
import java.io.Closeable
import java.io.FileNotFoundException
@ -41,21 +49,35 @@ import kotlin.concurrent.withLock
abstract class SyncManager<T: LocalResource<*>> @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String): Closeable {
// FIXME: remove all of the lateinit once we remove legacy (and make immutable)
// RemoteEntries and the likes are probably also just relevant for legacy
protected val isLegacy: Boolean = settings.isLegacy
protected val notificationManager: SyncNotification
protected val info: CollectionInfo
protected lateinit var info: CollectionInfo
protected var localCollection: LocalCollection<T>? = null
protected var httpClient: HttpClient
protected lateinit var etebaseLocalCache: EtebaseLocalCache
protected lateinit var etebase: com.etebase.client.Account
protected lateinit var colMgr: CollectionManager
protected lateinit var itemMgr: ItemManager
protected lateinit var cachedCollection: CachedCollection
// Sync counters
private var syncItemsTotal = 0
private var syncItemsDeleted = 0
private var syncItemsChanged = 0
protected var journal: JournalEntryManager? = null
private var _journalEntity: JournalEntity? = null
private var numDiscarded = 0
private val crypto: Crypto.CryptoManager
private lateinit var crypto: Crypto.CryptoManager
private val data: MyEntityDataStore
private lateinit var data: MyEntityDataStore
/**
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
@ -89,21 +111,31 @@ constructor(protected val context: Context, protected val account: Account, prot
// create HttpClient with given logger
httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
data = (context.applicationContext as App).data
val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType)
info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info
if (isLegacy) {
data = (context.applicationContext as App).data
val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType)
info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info
// dismiss previous error notifications
notificationManager = SyncNotification(context, journalUid, notificationId())
notificationManager.cancel()
Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version))
Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version))
if (journalEntity.encryptedKey != null) {
crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey)
if (journalEntity.encryptedKey != null) {
crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey)
} else {
crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!)
}
} else {
crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!)
etebaseLocalCache = EtebaseLocalCache.getInstance(context, accountName)
etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
colMgr = etebase.collectionManager
synchronized(etebaseLocalCache) {
cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid)!!
}
itemMgr = colMgr.getItemManager(cachedCollection.col)
}
// dismiss previous error notifications
notificationManager = SyncNotification(context, journalUid, notificationId())
notificationManager.cancel()
}
protected abstract fun notificationId(): Int
@ -114,6 +146,10 @@ constructor(protected val context: Context, protected val account: Account, prot
@TargetApi(21)
fun performSync() {
syncItemsTotal = 0
syncItemsDeleted = 0
syncItemsChanged = 0
var syncPhase = R.string.sync_phase_prepare
try {
Logger.log.info("Sync phase: " + context.getString(syncPhase))
@ -128,48 +164,101 @@ constructor(protected val context: Context, protected val account: Account, prot
Logger.log.info("Sync phase: " + context.getString(syncPhase))
prepareFetch()
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_fetch_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
fetchEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_remote_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
applyRemoteEntries()
} while (remoteEntries!!.size == MAX_FETCH)
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_prepare_local
Logger.log.info("Sync phase: " + context.getString(syncPhase))
prepareLocal()
/* Create journal entries out of local changes. */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_create_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
createLocalEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
/* FIXME: Skipping this now, because we already override with remote.
applyLocalEntries();
*/
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_push_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
pushEntries()
} while (localEntries!!.size == MAX_PUSH)
if (isLegacy) {
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_fetch_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
fetchEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_remote_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
applyRemoteEntries()
} while (remoteEntries!!.size == MAX_FETCH)
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_prepare_local
Logger.log.info("Sync phase: " + context.getString(syncPhase))
prepareLocal()
/* Create journal entries out of local changes. */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_create_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
createLocalEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
/* FIXME: Skipping this now, because we already override with remote.
applyLocalEntries();
*/
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_push_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
pushEntries()
} while (localEntries!!.size == MAX_PUSH)
} else {
var itemList: ItemListResponse?
var stoken = synchronized(etebaseLocalCache) {
etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid)
}
// Push local changes
var chunkPushItems: List<Item>
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_prepare_local
Logger.log.info("Sync phase: " + context.getString(syncPhase))
prepareLocal()
/* Create push items out of local changes. */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_create_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
chunkPushItems = createPushItems()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_push_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
pushItems(chunkPushItems)
} while (chunkPushItems.size == MAX_PUSH)
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_fetch_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
itemList = fetchItems(stoken)
if (itemList == null) {
break
}
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_remote_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
applyRemoteItems(itemList)
stoken = itemList.stoken
if (stoken != null) {
synchronized(etebaseLocalCache) {
etebaseLocalCache.collectionSaveStoken(cachedCollection.col.uid, stoken)
}
}
} while (!itemList!!.isDone)
}
/* Cleanup and finalize changes */
if (Thread.interrupted())
@ -187,6 +276,11 @@ constructor(protected val context: Context, protected val account: Account, prot
} catch (e: SSLHandshakeException) {
syncResult.stats.numIoExceptions++
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(syncErrorTitle, context.getString(syncPhase))
} catch (e: FileNotFoundException) {
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
@ -197,15 +291,21 @@ constructor(protected val context: Context, protected val account: Account, prot
} catch (e: Exceptions.ServiceUnavailableException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
} catch (e: TemporaryServerErrorException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
} catch (e: ConnectionException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
} catch (e: InterruptedException) {
// Restart sync if interrupted
syncResult.fullSyncRequested = true
} catch (e: Exceptions.IgnorableHttpException) {
// Ignore
} catch (e: Exception) {
if (e is Exceptions.UnauthorizedException) {
if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) {
syncResult.stats.numAuthExceptions++
} else if (e is Exceptions.HttpException) {
} else if (e is Exceptions.HttpException || e is HttpException) {
syncResult.stats.numParseExceptions++
} else if (e is CalendarStorageException || e is ContactsStorageException) {
syncResult.databaseError = true
@ -241,38 +341,28 @@ constructor(protected val context: Context, protected val account: Account, prot
private fun notifyUserOnSync() {
val changeNotification = context.defaultSharedPreferences.getBoolean(App.CHANGE_NOTIFICATION, true)
if (remoteEntries!!.isEmpty() || !changeNotification) {
if (!changeNotification || (syncItemsTotal == 0)) {
return
}
val notificationHelper = SyncNotification(context,
System.currentTimeMillis().toString(), notificationId())
var deleted = 0
var added = 0
var changed = 0
for (entry in remoteEntries!!) {
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
val action = cEntry.action
when (action) {
ADD -> added++
SyncEntry.Actions.DELETE -> deleted++
SyncEntry.Actions.CHANGE -> changed++
}
}
val resources = context.resources
val intent = ViewCollectionActivity.newIntent(context, account, info)
val intent = if (isLegacy) {
ViewCollectionActivity.newIntent(context, account, info)
} else {
CollectionActivity.newIntent(context, account, cachedCollection.col.uid)
}
notificationHelper.notify(syncSuccessfullyTitle,
String.format(context.getString(R.string.sync_successfully_modified),
resources.getQuantityString(R.plurals.sync_successfully,
remoteEntries!!.size, remoteEntries!!.size)),
syncItemsTotal, syncItemsTotal)),
String.format(context.getString(R.string.sync_successfully_modified_full),
resources.getQuantityString(R.plurals.sync_successfully,
added, added),
syncItemsChanged, syncItemsChanged),
resources.getQuantityString(R.plurals.sync_successfully,
changed, changed),
resources.getQuantityString(R.plurals.sync_successfully,
deleted, deleted)),
syncItemsDeleted, syncItemsDeleted)),
intent)
}
@ -287,6 +377,25 @@ constructor(protected val context: Context, protected val account: Account, prot
return true
}
protected abstract fun processItem(item: Item)
private fun persistItem(item: Item) {
synchronized(etebaseLocalCache) {
// FIXME: it's terrible that we are fetching and decrypting the item here - we really don't have to
val cached = etebaseLocalCache.itemGet(itemMgr, cachedCollection.col.uid, item.uid)
if (cached?.item?.etag != item.etag) {
syncItemsTotal++
if (item.isDeleted) {
syncItemsDeleted++
} else {
syncItemsChanged++
}
etebaseLocalCache.itemSet(itemMgr, cachedCollection.col.uid, item)
}
}
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
protected abstract fun processSyncEntryImpl(cEntry: SyncEntry)
@ -317,38 +426,54 @@ constructor(protected val context: Context, protected val account: Account, prot
throw e
}
}
when (syncEntry.action) {
ADD -> syncItemsChanged++
SyncEntry.Actions.DELETE -> syncItemsDeleted++
SyncEntry.Actions.CHANGE -> syncItemsChanged++
}
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.HttpException::class, InvalidCalendarException::class, InterruptedException::class)
protected fun applyLocalEntries() {
// FIXME: Need a better strategy
// We re-apply local entries so our changes override whatever was written in the remote.
val strTotal = localEntries!!.size.toString()
@Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class)
protected fun prepareFetch() {
if (isLegacy) {
remoteCTag = journalEntity.getLastUid(data)
} else {
remoteCTag = cachedCollection.col.stoken
}
}
private fun fetchItems(stoken: String?): ItemListResponse? {
if (remoteCTag != stoken) {
val ret = itemMgr.list(FetchOptions().stoken(stoken))
Logger.log.info("Fetched items. Done=${ret.isDone}")
return ret
} else {
Logger.log.info("Skipping fetch because local stoken == lastStoken (${remoteCTag})")
return null
}
}
private fun applyRemoteItems(itemList: ItemListResponse) {
val items = itemList.data
// Process new vcards from server
val size = items.size
var i = 0
for (entry in localEntries!!) {
for (item in items) {
if (Thread.interrupted()) {
throw InterruptedException()
}
i++
Logger.log.info("Processing (" + i.toString() + "/" + strTotal + ") " + entry.toString())
Logger.log.info("Processing (${i}/${size}) UID=${item.uid} Etag=${item.etag}")
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
if (cEntry.isAction(SyncEntry.Actions.DELETE)) {
continue
}
Logger.log.info("Processing resource for journal entry")
processSyncEntry(cEntry)
processItem(item)
persistItem(item)
}
}
@Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class)
protected fun prepareFetch() {
remoteCTag = journalEntity.getLastUid(data)
}
@Throws(Exceptions.HttpException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.IntegrityException::class)
protected fun fetchEntries() {
private fun fetchEntries() {
val count = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value()
if (remoteCTag != null && count == 0) {
// If we are updating an existing installation with no saved journal, we need to add
@ -377,11 +502,13 @@ constructor(protected val context: Context, protected val account: Account, prot
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class, InterruptedException::class)
protected fun applyRemoteEntries() {
private fun applyRemoteEntries() {
// Process new vcards from server
val strTotal = remoteEntries!!.size.toString()
var i = 0
syncItemsTotal += remoteEntries!!.size
for (entry in remoteEntries!!) {
if (Thread.interrupted()) {
throw InterruptedException()
@ -406,7 +533,7 @@ constructor(protected val context: Context, protected val account: Account, prot
}
@Throws(Exceptions.HttpException::class, IOException::class, ContactsStorageException::class, CalendarStorageException::class)
protected fun pushEntries() {
private fun pushEntries() {
// upload dirty contacts
var pushed = 0
// FIXME: Deal with failure (someone else uploaded before we go here)
@ -456,8 +583,128 @@ constructor(protected val context: Context, protected val account: Account, prot
}
}
private fun pushItems(chunkPushItems_: List<Item>) {
var chunkPushItems = chunkPushItems_
// upload dirty contacts
var pushed = 0
try {
if (!chunkPushItems.isEmpty()) {
val items = chunkPushItems
itemMgr.batch(items.toTypedArray())
// Persist the items
synchronized(etebaseLocalCache) {
val colUid = cachedCollection.col.uid
for (item in items) {
etebaseLocalCache.itemSet(itemMgr, colUid, item)
}
}
pushed += items.size
}
} finally {
// FIXME: A bit fragile, we assume the order in createPushItems
var left = pushed
for (local in localDeleted!!) {
if (pushed-- <= 0) {
break
}
local.delete()
}
if (left > 0) {
localDeleted = localDeleted?.drop(left)
chunkPushItems = chunkPushItems.drop(left - pushed)
}
left = pushed
var i = 0
for (local in localDirty) {
if (pushed-- <= 0) {
break
}
Logger.log.info("Added/changed resource with filename: " + local.fileName)
local.clearDirty(chunkPushItems[i].etag)
i++
}
if (left > 0) {
localDirty = localDirty.drop(left)
chunkPushItems.drop(left)
}
if (pushed > 0) {
Logger.log.severe("Unprocessed localentries left, this should never happen!")
}
}
}
private fun itemUpdateMtime(item: Item) {
val meta = item.meta
meta.setMtime(System.currentTimeMillis())
item.meta = meta
}
private fun createPushItems(): List<Item> {
val ret = LinkedList<Item>()
val colUid = cachedCollection.col.uid
synchronized(etebaseLocalCache) {
for (local in localDeleted!!) {
val item = etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!!.item
itemUpdateMtime(item)
item.delete()
ret.add(item)
if (ret.size == MAX_PUSH) {
return ret
}
}
}
synchronized(etebaseLocalCache) {
for (local in localDirty) {
val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!) else null
val item: Item
if (cacheItem != null) {
item = cacheItem.item
itemUpdateMtime(item)
} else {
val uid = UUID.randomUUID().toString()
val meta = ItemMetadata()
meta.name = uid
meta.mtime = System.currentTimeMillis()
item = itemMgr.create(meta, "")
local.prepareForUpload(item.uid, uid)
}
try {
item.setContent(local.content)
} catch (e: Exception) {
Logger.log.warning("Failed creating local entry ${local.uuid}")
if (local is LocalContact) {
Logger.log.warning("Contact with title ${local.contact?.displayName}")
} else if (local is LocalEvent) {
Logger.log.warning("Event with title ${local.event?.summary}")
} else if (local is LocalTask) {
Logger.log.warning("Task with title ${local.task?.summary}")
}
throw e
}
ret.add(item)
if (ret.size == MAX_PUSH) {
return ret
}
}
}
return ret
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
protected open fun createLocalEntries() {
private fun createLocalEntries() {
localEntries = LinkedList()
// Not saving, just creating a fake one until we load it from a local db
@ -510,7 +757,7 @@ constructor(protected val context: Context, protected val account: Account, prot
/**
*/
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
protected fun prepareLocal() {
protected open fun prepareLocal() {
localDeleted = processLocallyDeleted()
localDirty = localCollection!!.findDirty(MAX_PUSH)
// This is done after fetching the local dirty so all the ones we are using will be prepared
@ -527,7 +774,8 @@ constructor(protected val context: Context, protected val account: Account, prot
val localList = localCollection!!.findDeleted()
val ret = ArrayList<T>(localList.size)
if (journalEntity.isReadOnly) {
val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly))
if (readOnly) {
for (local in localList) {
Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}")
local.resetDeleted()
@ -541,8 +789,11 @@ constructor(protected val context: Context, protected val account: Account, prot
if (local.uuid != null) {
Logger.log.info(local.uuid + " has been deleted locally -> deleting from server")
} else {
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID")
local.prepareForUpload()
if (isLegacy) {
// It's done later for non-legacy
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID")
local.legacyPrepareForUpload(null)
}
}
ret.add(local)
@ -556,20 +807,22 @@ constructor(protected val context: Context, protected val account: Account, prot
@Throws(CalendarStorageException::class, ContactsStorageException::class)
protected open fun prepareDirty() {
if (journalEntity.isReadOnly) {
val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly))
if (readOnly) {
for (local in localDirty) {
Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}")
if (local.uuid == null) {
// If it was only local, delete.
local.delete()
} else {
local.clearDirty(local.uuid!!)
local.clearDirty(null)
}
numDiscarded++
}
localDirty = LinkedList()
} else {
} else if (isLegacy) {
// It's done later for non-legacy
// assign file names and UIDs to new entries
Logger.log.info("Looking for local entries without a uuid")
for (local in localDirty) {
@ -578,7 +831,7 @@ constructor(protected val context: Context, protected val account: Account, prot
}
Logger.log.fine("Found local record without file name; generating file name/UID if necessary")
local.prepareForUpload()
local.legacyPrepareForUpload(null)
}
}
}

@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.exceptions.*
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
@ -27,7 +28,8 @@ class SyncNotification(internal val context: Context, internal val notificationT
internal val notificationManager: NotificationManagerCompat
lateinit var detailsIntent: Intent
internal set
internal var messageString: Int = 0
internal var messageInt: Int = 0
internal var messageString: String? = null
private var throwable: Throwable? = null
@ -37,30 +39,33 @@ class SyncNotification(internal val context: Context, internal val notificationT
fun setThrowable(e: Throwable) {
throwable = e
if (e is Exceptions.UnauthorizedException) {
if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) {
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
messageString = R.string.sync_error_unauthorized
messageInt = R.string.sync_error_unauthorized
} else if (e is Exceptions.UserInactiveException) {
Logger.log.log(Level.SEVERE, "User inactive")
messageString = R.string.sync_error_user_inactive
} else if (e is Exceptions.ServiceUnavailableException) {
messageInt = R.string.sync_error_user_inactive
} else if (e is Exceptions.ServiceUnavailableException || e is TemporaryServerErrorException) {
Logger.log.log(Level.SEVERE, "Service unavailable")
messageString = R.string.sync_error_unavailable
messageInt = R.string.sync_error_unavailable
} else if (e is Exceptions.ReadOnlyException) {
Logger.log.log(Level.SEVERE, "Journal is read only", e)
messageString = R.string.sync_error_journal_readonly
} else if (e is Exceptions.HttpException) {
messageInt = R.string.sync_error_journal_readonly
} else if (e is PermissionDeniedException) {
Logger.log.log(Level.SEVERE, "Permission denied", e)
messageString = context.getString(R.string.sync_error_permission_denied, e.localizedMessage)
} else if (e is Exceptions.HttpException || e is ServerErrorException) {
Logger.log.log(Level.SEVERE, "HTTP Exception during sync", e)
messageString = R.string.sync_error_http_dav
messageInt = R.string.sync_error_http_dav
} else if (e is CalendarStorageException || e is ContactsStorageException || e is SQLiteException) {
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
messageString = R.string.sync_error_local_storage
messageInt = R.string.sync_error_local_storage
} else if (e is Exceptions.IntegrityException) {
Logger.log.log(Level.SEVERE, "Integrity error", e)
messageString = R.string.sync_error_integrity
messageInt = R.string.sync_error_integrity
} else {
Logger.log.log(Level.SEVERE, "Unknown sync error", e)
messageString = R.string.sync_error
messageInt = R.string.sync_error
}
detailsIntent = Intent(context, NotificationHandlerActivity::class.java)
@ -69,7 +74,7 @@ class SyncNotification(internal val context: Context, internal val notificationT
}
fun notify(title: String, state: String) {
val message = context.getString(messageString, state)
val message = messageString ?: context.getString(messageInt, state)
notify(title, message, null, detailsIntent)
}

@ -18,10 +18,7 @@ import android.os.Bundle
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -42,9 +39,6 @@ class TasksSyncAdapterService: SyncAdapterService() {
context: Context,
private val name: ProviderName
): SyncAdapter(context) {
override val syncErrorTitle = R.string.sync_error_tasks
override val notificationManager = SyncNotification(context, "journals-tasks", Constants.NOTIFICATION_TASK_SYNC)
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val taskProvider = TaskProvider.fromProviderClient(context, provider, name)
@ -63,8 +57,11 @@ class TasksSyncAdapterService: SyncAdapterService() {
RefreshCollections(account, CollectionInfo.Type.TASKS).run()
updateLocalTaskLists(taskProvider, account, accountSettings)
if (accountSettings.isLegacy) {
legacyUpdateLocalTaskLists(taskProvider, account, accountSettings)
} else {
updateLocalTaskLists(taskProvider, account, accountSettings)
}
val principal = accountSettings.uri?.toHttpUrlOrNull()!!
for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) {
@ -78,6 +75,50 @@ class TasksSyncAdapterService: SyncAdapterService() {
}
private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
val remote = HashMap<String, CachedCollection>()
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_TASKS }
}
for (collection in collections) {
remote[collection.col.uid] = collection
}
val local = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
val updateColors = settings.manageCalendarColors
// delete obsolete local calendar
for (taskList in local) {
val url = taskList.syncId
val collection = remote[url]
if (collection == null) {
Logger.log.fine("Deleting obsolete local taskList $url")
taskList.delete()
} else {
// remote CollectionInfo found for this local collection, update data
Logger.log.fine("Updating local taskList $url")
taskList.update(collection, updateColors)
// we already have a local taskList for this remote collection, don't take into consideration anymore
remote.remove(url)
}
}
// create new local calendars
for (url in remote.keys) {
val cachedCollection = remote[url]!!
Logger.log.info("Adding local calendar list $cachedCollection")
LocalTaskList.create(account, provider, cachedCollection)
}
}
private fun legacyUpdateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
val data = (context.applicationContext as App).data
var service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.TASKS)

@ -13,6 +13,7 @@ import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import at.bitfire.ical4android.Task
import com.etebase.client.Item
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
@ -43,7 +44,7 @@ class TasksSyncManager(
get() = context.getString(R.string.sync_error_tasks, account.name)
override val syncSuccessfullyTitle: String
get() = context.getString(R.string.sync_successfully_tasks, info.displayName,
get() = context.getString(R.string.sync_successfully_tasks, localTaskList().name!!,
account.name)
init {
@ -58,7 +59,9 @@ class TasksSyncManager(
if (!super.prepare())
return false
journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!)
if (isLegacy) {
journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!)
}
return true
}
@ -68,6 +71,32 @@ class TasksSyncManager(
return localCollection as LocalTaskList
}
override fun processItem(item: Item) {
val local = localCollection!!.findByFilename(item.uid)
if (!item.isDeleted) {
val inputReader = StringReader(String(item.content))
val tasks = Task.tasksFromReader(inputReader)
if (tasks.size == 0) {
Logger.log.warning("Received VCard without data, ignoring")
return
} else if (tasks.size > 1) {
Logger.log.warning("Received multiple VCALs, using first one")
}
val task = tasks[0]
processTask(item, task, local)
} else {
if (local != null) {
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
local.delete()
} else {
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
}
}
}
override fun processSyncEntryImpl(cEntry: SyncEntry) {
val inputReader = StringReader(cEntry.content)
@ -83,7 +112,7 @@ class TasksSyncManager(
val local = localCollection!!.findByUid(event.uid!!)
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processTask(event, local)
legacyProcessTask(event, local)
} else {
if (local != null) {
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
@ -94,7 +123,25 @@ class TasksSyncManager(
}
}
private fun processTask(newData: Task, _localTask: LocalTask?): LocalTask {
private fun processTask(item: Item, newData: Task, _localTask: LocalTask?): LocalTask {
var localTask = _localTask
// delete local Task, if it exists
if (localTask != null) {
Logger.log.info("Updating " + item.uid + " in local calendar")
localTask.eTag = item.etag
localTask.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding " + item.uid + " to local calendar")
localTask = LocalTask(localTaskList(), newData, item.uid, item.etag)
localTask.add()
syncResult.stats.numInserts++
}
return localTask
}
private fun legacyProcessTask(newData: Task, _localTask: LocalTask?): LocalTask {
var localTask = _localTask
// delete local Task, if it exists
if (localTask != null) {

@ -25,20 +25,27 @@ import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.*
import com.etebase.client.CollectionAccessLevel
import com.etebase.client.CollectionManager
import com.etebase.client.Utils
import com.etebase.client.exceptions.EtebaseException
import com.etesync.journalmanager.Crypto
import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.JournalAuthenticator
import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.MyEntityDataStore
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.etebase.CollectionActivity
import com.etesync.syncadapter.ui.etebase.InvitationsActivity
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
@ -52,6 +59,7 @@ import java.util.logging.Level
class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo>, Refreshable {
private lateinit var account: Account
private lateinit var settings: AccountSettings
private var accountInfo: AccountInfo? = null
internal var listCalDAV: ListView? = null
@ -64,22 +72,30 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
private val onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, _ ->
val list = parent as ListView
val adapter = list.adapter as ArrayAdapter<*>
val journalEntity = adapter.getItem(position) as JournalEntity
val info = journalEntity.getInfo()
val info = adapter.getItem(position) as CollectionListItemInfo
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info))
if (settings.isLegacy) {
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!))
} else {
startActivity(CollectionActivity.newIntent(this@AccountActivity, account, info.uid))
}
}
private val formattedFingerprint: String?
get() {
try {
val settings = AccountSettings(this, account)
return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey)
if (settings.isLegacy) {
val settings = AccountSettings(this, account)
return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey)
} else {
val etebase = EtebaseLocalCache.getEtebase(this, HttpClient.sharedClient, settings)
val invitationManager = etebase.invitationManager
return Utils.prettyFingerprint(invitationManager.pubkey)
}
} catch (e: Exception) {
e.printStackTrace()
return null
return e.localizedMessage
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -87,6 +103,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
account = intent.getParcelableExtra(EXTRA_ACCOUNT)
title = account.name
settings = AccountSettings(this, account)
setContentView(R.layout.activity_account)
@ -131,13 +148,19 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true)
}
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null)
if (settings.isLegacy) {
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_account, menu)
if (settings.isLegacy) {
val invitations = menu.findItem(R.id.invitations)
invitations.setVisible(false)
}
return true
}
@ -167,6 +190,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
dialog.show()
}
R.id.invitations -> {
val intent = InvitationsActivity.newIntent(this, account)
startActivity(intent)
}
else -> return super.onOptionsItemSelected(item)
}
return true
@ -195,19 +222,31 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
val info: CollectionInfo
when (item.itemId) {
R.id.create_calendar -> {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.CALENDAR
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
if (settings.isLegacy) {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.CALENDAR
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
} else {
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_CALENDAR))
}
}
R.id.create_tasklist -> {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.TASKS
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
if (settings.isLegacy) {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.TASKS
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
} else {
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_TASKS))
}
}
R.id.create_addressbook -> {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.ADDRESS_BOOK
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
if (settings.isLegacy) {
info = CollectionInfo()
info.enumType = CollectionInfo.Type.ADDRESS_BOOK
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
} else {
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_ADDRESS_BOOK))
}
}
R.id.install_tasksorg -> {
installPackage(tasksOrgPackage)
@ -227,10 +266,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
internal var taskdav: ServiceInfo? = null
class ServiceInfo {
internal var id: Long = 0
internal var refreshing: Boolean = false
internal var journals: List<JournalEntity>? = null
internal var infos: List<CollectionListItemInfo>? = null
}
}
@ -254,7 +292,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
listCardDAV!!.setAlpha(if (info.carddav!!.refreshing) 0.5f else 1f)
val adapter = CollectionListAdapter(this, account)
adapter.addAll(info.carddav!!.journals!!)
adapter.addAll(info.carddav!!.infos!!)
listCardDAV!!.adapter = adapter
listCardDAV!!.onItemClickListener = onItemClickListener
}
@ -268,7 +306,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
listCalDAV!!.setAlpha(if (info.caldav!!.refreshing) 0.5f else 1f)
val adapter = CollectionListAdapter(this, account)
adapter.addAll(info.caldav!!.journals!!)
adapter.addAll(info.caldav!!.infos!!)
listCalDAV!!.adapter = adapter
listCalDAV!!.onItemClickListener = onItemClickListener
}
@ -282,7 +320,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
listTaskDAV!!.setAlpha(if (info.taskdav!!.refreshing) 0.5f else 1f)
val adapter = CollectionListAdapter(this, account)
adapter.addAll(info.taskdav!!.journals!!)
adapter.addAll(info.taskdav!!.infos!!)
listTaskDAV!!.adapter = adapter
listTaskDAV!!.onItemClickListener = onItemClickListener
@ -342,50 +380,121 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
forceLoad()
}
private fun getLegacyJournals(data: MyEntityDataStore, serviceEntity: ServiceEntity): List<CollectionListItemInfo> {
return JournalEntity.getJournals(data, serviceEntity).map {
val info = it.info
val isAdmin = it.isOwner(account.name)
CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description ?: "", info.color, it.isReadOnly, isAdmin, info)
}
}
private fun getCollections(etebaseLocalCache: EtebaseLocalCache, colMgr: CollectionManager, type: CollectionInfo.Type): List<CollectionListItemInfo> {
val strType = when (type) {
CollectionInfo.Type.ADDRESS_BOOK -> ETEBASE_TYPE_ADDRESS_BOOK
CollectionInfo.Type.CALENDAR -> ETEBASE_TYPE_CALENDAR
CollectionInfo.Type.TASKS -> ETEBASE_TYPE_TASKS
}
synchronized(etebaseLocalCache) {
return etebaseLocalCache.collectionList(colMgr).map {
val meta = it.meta
if (strType != meta.collectionType) {
return@map null
}
val accessLevel = it.col.accessLevel
val isReadOnly = accessLevel == CollectionAccessLevel.ReadOnly
val isAdmin = accessLevel == CollectionAccessLevel.Admin
val metaColor = meta.color
val color = if (!metaColor.isNullOrBlank()) LocalCalendar.parseColor(metaColor) else null
CollectionListItemInfo(it.col.uid, type, meta.name, meta.description
?: "", color, isReadOnly, isAdmin, null)
}.filterNotNull()
}
}
override fun loadInBackground(): AccountInfo {
val info = AccountInfo()
val settings: AccountSettings
try {
settings = AccountSettings(context, account)
} catch (e: InvalidAccountException) {
return info
}
if (settings.isLegacy) {
val data = (context.applicationContext as App).data
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
val id = serviceEntity.id.toLong()
val service = serviceEntity.type!!
when (service) {
CollectionInfo.Type.ADDRESS_BOOK -> {
info.carddav = AccountInfo.ServiceInfo()
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority)
info.carddav!!.infos = getLegacyJournals(data, serviceEntity)
val accountManager = AccountManager.get(context)
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, addrBookAccount, null)
try {
if (account == addressBook.mainAccount)
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
} catch (e: ContactsStorageException) {
}
val data = (context.applicationContext as App).data
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
val id = serviceEntity.id.toLong()
val service = serviceEntity.type!!
when (service) {
CollectionInfo.Type.ADDRESS_BOOK -> {
info.carddav = AccountInfo.ServiceInfo()
info.carddav!!.id = id
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority)
info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity)
val accountManager = AccountManager.get(context)
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, addrBookAccount, null)
try {
if (account == addressBook.mainAccount)
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
} catch (e: ContactsStorageException) {
}
}
}
CollectionInfo.Type.CALENDAR -> {
info.caldav = AccountInfo.ServiceInfo()
info.caldav!!.id = id
info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity)
}
CollectionInfo.Type.TASKS -> {
info.taskdav = AccountInfo.ServiceInfo()
info.taskdav!!.id = id
info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
OPENTASK_PROVIDERS.any {
ContentResolver.isSyncActive(account, it.authority)
}
info.taskdav!!.journals = JournalEntity.getJournals(data, serviceEntity)
CollectionInfo.Type.CALENDAR -> {
info.caldav = AccountInfo.ServiceInfo()
info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
info.caldav!!.infos = getLegacyJournals(data, serviceEntity)
}
CollectionInfo.Type.TASKS -> {
info.taskdav = AccountInfo.ServiceInfo()
info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
OPENTASK_PROVIDERS.any {
ContentResolver.isSyncActive(account, it.authority)
}
info.taskdav!!.infos = getLegacyJournals(data, serviceEntity)
}
}
}
return info
}
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val httpClient = HttpClient.Builder(context).build().okHttpClient
val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings)
val colMgr = etebase.collectionManager
info.carddav = AccountInfo.ServiceInfo()
info.carddav!!.refreshing = ContentResolver.isSyncActive(account, App.addressBooksAuthority)
info.carddav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.ADDRESS_BOOK)
val accountManager = AccountManager.get(context)
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(context, addrBookAccount, null)
try {
if (account == addressBook.mainAccount)
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
} catch (e: ContactsStorageException) {
}
}
info.caldav = AccountInfo.ServiceInfo()
info.caldav!!.refreshing = ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
info.caldav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.CALENDAR)
info.taskdav = AccountInfo.ServiceInfo()
info.taskdav!!.refreshing = OPENTASK_PROVIDERS.any {
ContentResolver.isSyncActive(account, it.authority)
}
info.taskdav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS)
return info
}
}
@ -393,15 +502,16 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
/* LIST ADAPTERS */
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<JournalEntity>(context, R.layout.account_collection_item) {
data class CollectionListItemInfo(val uid: String, val enumType: CollectionInfo.Type, val displayName: String, val description: String, val color: Int?, val isReadOnly: Boolean, val isAdmin: Boolean, val legacyInfo: CollectionInfo?)
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<CollectionListItemInfo>(context, R.layout.account_collection_item) {
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
var v = _v
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.account_collection_item, parent, false)
val journalEntity = getItem(position)
val info = journalEntity!!.info
val info = getItem(position)!!
var tv = v!!.findViewById<View>(R.id.title) as TextView
tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName
@ -422,10 +532,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
val readOnly = v.findViewById<View>(R.id.read_only)
readOnly.visibility = if (journalEntity.isReadOnly) View.VISIBLE else View.GONE
readOnly.visibility = if (info.isReadOnly) View.VISIBLE else View.GONE
val shared = v.findViewById<View>(R.id.shared)
val isOwner = journalEntity.isOwner(account.name)
val isOwner = info.isAdmin
shared.visibility = if (isOwner) View.GONE else View.VISIBLE
return v
@ -437,17 +547,32 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
private fun deleteAccount() {
val accountManager = AccountManager.get(this)
val settings = AccountSettings(this@AccountActivity, account)
val authToken = settings.authToken
val principal = settings.uri?.toHttpUrlOrNull()
doAsync {
try {
val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient
val journalAuthenticator = JournalAuthenticator(httpClient, principal!!)
journalAuthenticator.invalidateAuthToken(authToken)
} catch (e: Exceptions.HttpException) {
// Ignore failures for now
Logger.log.warning(e.toString())
if (settings.isLegacy) {
val authToken = settings.authToken
val principal = settings.uri?.toHttpUrlOrNull()
try {
val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient
val journalAuthenticator = JournalAuthenticator(httpClient, principal!!)
journalAuthenticator.invalidateAuthToken(authToken)
} catch (e: Exceptions.HttpException) {
// Ignore failures for now
Logger.log.warning(e.toString())
}
} else {
val etebaseLocalCache = EtebaseLocalCache.getInstance(this@AccountActivity, account.name)
etebaseLocalCache.clearUserCache()
try {
val httpClient = HttpClient.Builder(this@AccountActivity).build()
val etebase = EtebaseLocalCache.getEtebase(this@AccountActivity, httpClient.okHttpClient, settings)
etebase.logout()
} catch(e: EtebaseException) {
// Ignore failures for now
Logger.log.warning(e.toString())
}
}
}

@ -32,7 +32,7 @@ import com.etesync.syncadapter.R
class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks<Array<Account>>, AdapterView.OnItemClickListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
listAdapter = AccountListAdapter(context!!)
listAdapter = AccountListAdapter(requireContext())
return inflater.inflate(R.layout.account_list, container, false)
}
@ -58,7 +58,7 @@ class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks<Array<
// loader
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Array<Account>> {
return AccountLoader(context!!)
return AccountLoader(requireContext())
}
override fun onLoadFinished(loader: Loader<Array<Account>>, accounts: Array<Account>) {

@ -17,12 +17,13 @@ import android.os.Bundle
import android.provider.CalendarContract
import android.text.TextUtils
import android.view.MenuItem
import android.widget.Toast
import androidx.core.app.NavUtils
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader
import androidx.preference.*
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
@ -43,7 +44,8 @@ class AccountSettingsActivity : BaseActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
val frag = AccountSettingsFragment()
val settings = AccountSettings(this, account)
val frag: Fragment = if (settings.isLegacy) LegacyAccountSettingsFragment() else AccountSettingsFragment()
frag.arguments = intent.extras
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, frag)
@ -60,139 +62,226 @@ class AccountSettingsActivity : BaseActivity() {
} else
return false
}
}
class AccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
internal lateinit var account: Account
class AccountSettingsFragment() : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
internal lateinit var account: Account
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = arguments?.getParcelable(KEY_ACCOUNT)!!
account = arguments?.getParcelable(KEY_ACCOUNT)!!
loaderManager.initLoader(0, arguments, this)
}
loaderManager.initLoader(0, arguments, this)
}
override fun onCreatePreferences(bundle: Bundle, s: String) {
addPreferencesFromResource(R.xml.settings_account)
}
override fun onCreatePreferences(bundle: Bundle, s: String) {
addPreferencesFromResource(R.xml.settings_account)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
}
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
if (settings == null) {
activity!!.finish()
return
}
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
if (settings == null) {
activity!!.finish()
return
}
// Category: dashboard
val prefManageAccount = findPreference("manage_account")
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
Toast.makeText(requireContext(), "Not yet supported", Toast.LENGTH_LONG).show()
true
}
// Category: dashboard
val prefManageAccount = findPreference("manage_account")
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build())
true
}
// Category: encryption
val prefEncryptionPassword = findPreference("password")
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
true
}
// category: authentication
val prefPassword = findPreference("password") as EditTextPreference
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null
LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null)
val prefSync = findPreference("sync_interval") as ListPreference
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
if (syncInterval != null) {
prefSync.value = syncInterval.toString()
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
prefSync.setSummary(R.string.settings_sync_summary_manually)
else
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val newInterval = java.lang.Long.parseLong(newValue as String)
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
OPENTASK_PROVIDERS.forEach {
settings.setSyncInterval(it.authority, newInterval)
}
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
} else {
prefSync.isEnabled = false
prefSync.setSummary(R.string.settings_sync_summary_not_available)
}
// Category: encryption
val prefEncryptionPassword = findPreference("encryption_password")
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
true
}
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
prefWifiOnly.isChecked = settings.syncWifiOnly
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
settings.setSyncWiFiOnly(wifiOnly as Boolean)
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
val prefSync = findPreference("sync_interval") as ListPreference
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
if (syncInterval != null) {
prefSync.value = syncInterval.toString()
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
prefSync.setSummary(R.string.settings_sync_summary_manually)
else
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val newInterval = java.lang.Long.parseLong(newValue as String)
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
OPENTASK_PROVIDERS.forEach {
settings.setSyncInterval(it.authority, newInterval)
}
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
} else {
prefSync.isEnabled = false
prefSync.setSummary(R.string.settings_sync_summary_not_available)
}
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
val onlySSID = settings.syncWifiOnlySSID
prefWifiOnlySSID.text = onlySSID
if (onlySSID != null)
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
else
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val ssid = newValue as String
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
}
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
prefWifiOnly.isChecked = settings.syncWifiOnly
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
settings.setSyncWiFiOnly(wifiOnly as Boolean)
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
val onlySSID = settings.syncWifiOnlySSID
prefWifiOnlySSID.text = onlySSID
if (onlySSID != null)
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
else
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val ssid = newValue as String
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
false
}
}
}
class LegacyAccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
internal lateinit var account: Account
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = arguments?.getParcelable(KEY_ACCOUNT)!!
loaderManager.initLoader(0, arguments, this)
}
override fun onCreatePreferences(bundle: Bundle, s: String) {
addPreferencesFromResource(R.xml.settings_account_legacy)
}
private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader<AccountSettings>(context), SyncStatusObserver {
internal lateinit var listenerHandle: Any
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
}
override fun onStartLoading() {
forceLoad()
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
if (settings == null) {
activity!!.finish()
return
}
override fun onStopLoading() {
ContentResolver.removeStatusChangeListener(listenerHandle)
// Category: dashboard
val prefManageAccount = findPreference("manage_account")
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build())
true
}
override fun abandon() {
onStopLoading()
// category: authentication
val prefPassword = findPreference("password") as EditTextPreference
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null
LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null)
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
false
}
override fun loadInBackground(): AccountSettings? {
val settings: AccountSettings
try {
settings = AccountSettings(context, account)
} catch (e: InvalidAccountException) {
return null
}
// Category: encryption
val prefEncryptionPassword = findPreference("encryption_password")
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
true
}
return settings
val prefSync = findPreference("sync_interval") as ListPreference
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
if (syncInterval != null) {
prefSync.value = syncInterval.toString()
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
prefSync.setSummary(R.string.settings_sync_summary_manually)
else
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val newInterval = java.lang.Long.parseLong(newValue as String)
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
OPENTASK_PROVIDERS.forEach {
settings.setSyncInterval(it.authority, newInterval)
}
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
false
}
} else {
prefSync.isEnabled = false
prefSync.setSummary(R.string.settings_sync_summary_not_available)
}
override fun onStatusChanged(which: Int) {
Logger.log.fine("Reloading account settings")
forceLoad()
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
prefWifiOnly.isChecked = settings.syncWifiOnly
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
settings.setSyncWiFiOnly(wifiOnly as Boolean)
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
false
}
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
val onlySSID = settings.syncWifiOnlySSID
prefWifiOnlySSID.text = onlySSID
if (onlySSID != null)
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
else
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val ssid = newValue as String
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
false
}
}
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
}
private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader<AccountSettings>(context), SyncStatusObserver {
internal lateinit var listenerHandle: Any
override fun onStartLoading() {
forceLoad()
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
}
override fun onStopLoading() {
ContentResolver.removeStatusChangeListener(listenerHandle)
}
override fun abandon() {
onStopLoading()
}
override fun loadInBackground(): AccountSettings? {
val settings: AccountSettings
try {
settings = AccountSettings(context, account)
} catch (e: InvalidAccountException) {
return null
}
return settings
}
override fun onStatusChanged(which: Int) {
Logger.log.fine("Reloading account settings")
forceLoad()
}
}

@ -15,6 +15,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.etebase.client.Client
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R
@ -53,7 +54,7 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
AlertDialog.Builder(this)
.setTitle(R.string.wrong_encryption_password)
.setIcon(R.drawable.ic_error_dark)
.setMessage(getString(R.string.wrong_encryption_password_content, e.localizedMessage))
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.ok) { _, _ ->
// dismiss
}.show()
@ -62,6 +63,45 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
fun changePasswordDo(old_password: String, new_password: String) {
val settings = AccountSettings(this, account)
if (settings.isLegacy) {
legacyChangePasswordDo(settings, old_password, new_password)
return
}
doAsync {
val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity).setForeground(true).build().okHttpClient
try {
Logger.log.info("Loging in with old password")
val client = Client.create(httpClient, settings.uri?.toString())
val etebase = com.etebase.client.Account.login(client, account.name, old_password)
Logger.log.info("Login successful")
etebase.changePassword(new_password)
settings.etebaseSession = etebase.save(null)
uiThread {
progress.dismiss()
AlertDialog.Builder(this@ChangeEncryptionPasswordActivity)
.setTitle(R.string.change_encryption_password_success_title)
.setMessage(R.string.change_encryption_password_success_body)
.setPositiveButton(android.R.string.ok) { _, _ ->
this@ChangeEncryptionPasswordActivity.finish()
}.show()
requestSync(applicationContext, account)
}
} catch (e: Exception) {
uiThread {
changePasswordError(e)
}
return@doAsync
}
}
}
fun legacyChangePasswordDo(settings: AccountSettings, old_password: String, new_password: String) {
doAsync {
val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity, settings).setForeground(false).build().okHttpClient
@ -175,8 +215,8 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
.setPositiveButton(android.R.string.yes) { _, _ ->
changePasswordDo(old_password, new_password)
progress = ProgressDialog(this)
progress.setTitle(R.string.login_encryption_setup_title)
progress.setMessage(getString(R.string.login_encryption_setup))
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
progress.setCancelable(false)

@ -106,13 +106,13 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val member = listAdapter?.getItem(position) as JournalManager.Member
AlertDialog.Builder(activity!!)
AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.collection_members_remove_title)
.setMessage(getString(R.string.collection_members_remove, member.user))
.setPositiveButton(android.R.string.yes) { dialog, which ->
val frag = RemoveMemberFragment.newInstance(account, info, member.user!!)
frag.show(fragmentManager!!, null)
frag.show(requireFragmentManager(), null)
}
.setNegativeButton(android.R.string.no) { dialog, which -> }.show()
}

@ -0,0 +1,170 @@
package com.etesync.syncadapter.ui.etebase
import android.accounts.Account
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.commit
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.CollectionManager
import com.etebase.client.CollectionMetadata
import com.etesync.syncadapter.*
import com.etesync.syncadapter.ui.BaseActivity
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
class CollectionActivity() : BaseActivity() {
private lateinit var account: Account
private val model: AccountViewModel by viewModels()
private val collectionModel: CollectionViewModel by viewModels()
private val itemsModel: ItemsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
val colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)
val colType = intent.extras!!.getString(EXTRA_COLLECTION_TYPE)
setContentView(R.layout.etebase_collection_activity)
if (savedInstanceState == null) {
model.loadAccount(this, account)
if (colUid != null) {
model.observe(this) {
collectionModel.loadCollection(it, colUid)
collectionModel.observe(this) { cachedCollection ->
itemsModel.loadItems(it, cachedCollection)
}
}
supportFragmentManager.commit {
replace(R.id.fragment_container, ViewCollectionFragment())
}
} else if (colType != null) {
model.observe(this) {
doAsync {
val meta = CollectionMetadata(colType, "")
val cachedCollection = CachedCollection(it.colMgr.create(meta, ""), meta)
uiThread {
supportFragmentManager.commit {
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection, true))
}
}
}
}
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
companion object {
private val EXTRA_ACCOUNT = "account"
private val EXTRA_COLLECTION_UID = "collectionUid"
private val EXTRA_COLLECTION_TYPE = "collectionType"
fun newIntent(context: Context, account: Account, colUid: String): Intent {
val intent = Intent(context, CollectionActivity::class.java)
intent.putExtra(EXTRA_ACCOUNT, account)
intent.putExtra(EXTRA_COLLECTION_UID, colUid)
return intent
}
fun newCreateCollectionIntent(context: Context, account: Account, colType: String): Intent {
val intent = Intent(context, CollectionActivity::class.java)
intent.putExtra(EXTRA_ACCOUNT, account)
intent.putExtra(EXTRA_COLLECTION_TYPE, colType)
return intent
}
}
}
class AccountViewModel : ViewModel() {
private val holder = MutableLiveData<AccountHolder>()
fun loadAccount(context: Context, account: Account) {
doAsync {
val settings = AccountSettings(context, account)
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings)
val colMgr = etebase.collectionManager
uiThread {
holder.value = AccountHolder(
account,
etebaseLocalCache,
etebase,
colMgr
)
}
}
}
fun observe(owner: LifecycleOwner, observer: (AccountHolder) -> Unit) =
holder.observe(owner, observer)
val value: AccountHolder?
get() = holder.value
}
data class AccountHolder(val account: Account, val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager)
class CollectionViewModel : ViewModel() {
private val collection = MutableLiveData<CachedCollection>()
fun loadCollection(accountHolder: AccountHolder, colUid: String) {
doAsync {
val etebaseLocalCache = accountHolder.etebaseLocalCache
val colMgr = accountHolder.colMgr
val cachedCollection = synchronized(etebaseLocalCache) {
etebaseLocalCache.collectionGet(colMgr, colUid)!!
}
uiThread {
collection.value = cachedCollection
}
}
}
fun observe(owner: LifecycleOwner, observer: (CachedCollection) -> Unit) =
collection.observe(owner, observer)
val value: CachedCollection?
get() = collection.value
}
class ItemsViewModel : ViewModel() {
private val cachedItems = MutableLiveData<List<CachedItem>>()
fun loadItems(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) {
doAsync {
val col = cachedCollection.col
val itemMgr = accountCollectionHolder.colMgr.getItemManager(col)
val items = accountCollectionHolder.etebaseLocalCache.itemList(itemMgr, col.uid, withDeleted = true)
uiThread {
cachedItems.value = items
}
}
}
fun observe(owner: LifecycleOwner, observer: (List<CachedItem>) -> Unit) =
cachedItems.observe(owner, observer)
val value: List<CachedItem>?
get() = cachedItems.value
}
class LoadingViewModel : ViewModel() {
private val loading = MutableLiveData<Boolean>()
fun setLoading(value: Boolean) {
loading.value = value
}
fun observe(owner: LifecycleOwner, observer: (Boolean) -> Unit) =
loading.observe(owner, observer)
}

@ -0,0 +1,532 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.view.*
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.fragment.app.activityViewModels
import androidx.viewpager.widget.ViewPager
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.Contact
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.CachedItem
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.resource.*
import com.etesync.syncadapter.ui.BaseActivity
import com.etesync.syncadapter.utils.EventEmailInvitation
import com.etesync.syncadapter.utils.TaskProviderHandling
import com.google.android.material.tabs.TabLayout
import ezvcard.util.PartialDate
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.IOException
import java.io.StringReader
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Future
class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
private var emailInvitationEvent: Event? = null
private var emailInvitationEventString: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.journal_item_activity, container, false)
setHasOptionsMenu(true)
if (savedInstanceState == null) {
collectionModel.observe(this) {
(activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name
if (container != null) {
initUi(inflater, ret, it)
}
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
val viewPager = v.findViewById<ViewPager>(R.id.viewpager)
viewPager.adapter = TabsAdapter(childFragmentManager, this, requireContext(), cachedCollection, cachedItem)
val tabLayout = v.findViewById<TabLayout>(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
v.findViewById<View>(R.id.journal_list_item).visibility = View.GONE
}
fun allowSendEmail(event: Event?, icsContent: String) {
emailInvitationEvent = event
emailInvitationEventString = icsContent
activity?.invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.collection_item_fragment, menu)
menu.setGroupVisible(R.id.journal_item_menu_event_invite, emailInvitationEvent != null)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val accountHolder = model.value!!
when (item.itemId) {
R.id.on_send_event_invite -> {
val account = accountHolder.account
val intent = EventEmailInvitation(requireContext(), account).createIntent(emailInvitationEvent!!, emailInvitationEventString!!)
startActivity(intent)
}
R.id.on_restore_item -> {
restoreItem(accountHolder)
}
}
return super.onOptionsItemSelected(item)
}
fun restoreItem(accountHolder: AccountHolder) {
// FIXME: This code makes the assumption that providers are all available. May not be true for tasks, and potentially others too.
val context = requireContext()
val account = accountHolder.account
val cachedCol = collectionModel.value!!
when (cachedCol.meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!
val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, cachedCol.col.uid)!!
val event = Event.eventsFromReader(StringReader(cachedItem.content))[0]
var localEvent = localCalendar.findByUid(event.uid!!)
if (localEvent != null) {
localEvent.updateAsDirty(event)
} else {
localEvent = LocalEvent(localCalendar, event, event.uid, null)
localEvent.addAsDirty()
}
}
Constants.ETEBASE_TYPE_TASKS -> {
TaskProviderHandling.getWantedTaskSyncProvider(context)?.let {
val provider = TaskProvider.acquire(context, it)!!
val localTaskList = LocalTaskList.findByName(account, provider, LocalTaskList.Factory, cachedCol.col.uid)!!
val task = Task.tasksFromReader(StringReader(cachedItem.content))[0]
var localTask = localTaskList.findByUid(task.uid!!)
if (localTask != null) {
localTask.updateAsDirty(task)
} else {
localTask = LocalTask(localTaskList, task, task.uid, null)
localTask.addAsDirty()
}
}
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, cachedCol.col.uid)!!
val contact = Contact.fromReader(StringReader(cachedItem.content), null)[0]
if (contact.group) {
// FIXME: not currently supported
} else {
var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact?
if (localContact != null) {
localContact.updateAsDirty(contact)
} else {
localContact = LocalContact(localAddressBook, contact, contact.uid, null)
localContact.createAsDirty()
}
}
}
}
val dialog = AlertDialog.Builder(context)
.setTitle(R.string.journal_item_restore_action)
.setIcon(R.drawable.ic_restore_black)
.setMessage(R.string.journal_item_restore_dialog_body)
.setPositiveButton(android.R.string.ok) { dialog, which ->
// dismiss
}
.create()
dialog.show()
}
}
private class TabsAdapter(fm: FragmentManager, private val mainFragment: CollectionItemFragment, private val context: Context, private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : FragmentPagerAdapter(fm) {
override fun getCount(): Int {
// FIXME: Make it depend on info enumType (only have non-raw for known types)
return 3
}
override fun getPageTitle(position: Int): CharSequence? {
return if (position == 0) {
context.getString(R.string.journal_item_tab_main)
} else if (position == 1) {
context.getString(R.string.journal_item_tab_raw)
} else {
context.getString(R.string.journal_item_tab_revisions)
}
}
override fun getItem(position: Int): Fragment {
return if (position == 0) {
PrettyFragment(mainFragment, cachedCollection, cachedItem.content)
} else if (position == 1) {
TextFragment(cachedItem.content)
} else {
ItemRevisionsListFragment(cachedCollection, cachedItem)
}
}
}
class TextFragment(private val content: String) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.text_fragment, container, false)
val tv = v.findViewById<View>(R.id.content) as TextView
tv.text = content
return v
}
}
class PrettyFragment(private val mainFragment: CollectionItemFragment, private val cachedCollection: CachedCollection, private val content: String) : Fragment() {
private var asyncTask: Future<Unit>? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var v: View? = null
when (cachedCollection.meta.collectionType) {
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
v = inflater.inflate(R.layout.contact_info, container, false)
asyncTask = loadContactTask(v)
}
Constants.ETEBASE_TYPE_CALENDAR -> {
v = inflater.inflate(R.layout.event_info, container, false)
asyncTask = loadEventTask(v)
}
Constants.ETEBASE_TYPE_TASKS -> {
v = inflater.inflate(R.layout.task_info, container, false)
asyncTask = loadTaskTask(v)
}
}
return v
}
override fun onDestroyView() {
super.onDestroyView()
if (asyncTask != null)
asyncTask!!.cancel(true)
}
private fun loadEventTask(view: View): Future<Unit> {
return doAsync {
var event: Event? = null
val inputReader = StringReader(content)
try {
event = Event.eventsFromReader(inputReader, null)[0]
} catch (e: InvalidCalendarException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
if (event != null) {
uiThread {
val loader = view.findViewById<View>(R.id.event_info_loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.event_info_scroll_view)
contentContainer.visibility = View.VISIBLE
setTextViewText(view, R.id.title, event.summary)
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context))
setTextViewText(view, R.id.where, event.location)
val organizer = event.organizer
if (organizer != null) {
val tv = view.findViewById<View>(R.id.organizer) as TextView
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizerView = view.findViewById<View>(R.id.organizer_container)
organizerView.visibility = View.GONE
}
setTextViewText(view, R.id.description, event.description)
var first = true
var sb = StringBuilder()
for (attendee in event.attendees) {
if (first) {
first = false
sb.append(getString(R.string.journal_item_attendees)).append(": ")
} else {
sb.append(", ")
}
sb.append(attendee.calAddress.toString().replaceFirst("mailto:".toRegex(), ""))
}
setTextViewText(view, R.id.attendees, sb.toString())
first = true
sb = StringBuilder()
for (alarm in event.alarms) {
if (first) {
first = false
sb.append(getString(R.string.journal_item_reminders)).append(": ")
} else {
sb.append(", ")
}
sb.append(alarm.trigger.value)
}
setTextViewText(view, R.id.reminders, sb.toString())
if (event.attendees.isNotEmpty()) {
mainFragment.allowSendEmail(event, content)
}
}
}
}
}
private fun loadTaskTask(view: View): Future<Unit> {
return doAsync {
var task: Task? = null
val inputReader = StringReader(content)
try {
task = Task.tasksFromReader(inputReader)[0]
} catch (e: InvalidCalendarException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
if (task != null) {
uiThread {
val loader = view.findViewById<View>(R.id.task_info_loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.task_info_scroll_view)
contentContainer.visibility = View.VISIBLE
setTextViewText(view, R.id.title, task.summary)
setTextViewText(view, R.id.where, task.location)
val organizer = task.organizer
if (organizer != null) {
val tv = view.findViewById<View>(R.id.organizer) as TextView
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizerView = view.findViewById<View>(R.id.organizer_container)
organizerView.visibility = View.GONE
}
setTextViewText(view, R.id.description, task.description)
}
}
}
}
private fun loadContactTask(view: View): Future<Unit> {
return doAsync {
var contact: Contact? = null
val reader = StringReader(content)
try {
contact = Contact.fromReader(reader, null)[0]
} catch (e: IOException) {
e.printStackTrace()
}
if (contact != null) {
uiThread {
val loader = view.findViewById<View>(R.id.loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.content_container)
contentContainer.visibility = View.VISIBLE
val tv = view.findViewById<View>(R.id.display_name) as TextView
tv.text = contact.displayName
if (contact.group) {
showGroup(contact)
} else {
showContact(contact)
}
}
}
}
}
private fun showGroup(contact: Contact) {
val view = requireView()
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member_count), null, contact.members.size.toString())
for (member in contact.members) {
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member), null, member)
}
}
private fun showContact(contact: Contact) {
val view = requireView()
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
val aboutCard = view.findViewById<View>(R.id.about_card) as ViewGroup
aboutCard.findViewById<View>(R.id.title_container).visibility = View.VISIBLE
// TEL
for (labeledPhone in contact.phoneNumbers) {
val types = labeledPhone.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.text)
}
// EMAIL
for (labeledEmail in contact.emails) {
val types = labeledEmail.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.value)
}
// ORG, TITLE, ROLE
if (contact.organization != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0])
}
if (contact.jobDescription != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle)
}
// IMPP
for (labeledImpp in contact.impps) {
addInfoItem(view.context, mainCard, getString(R.string.journal_item_impp), labeledImpp.property.protocol, labeledImpp.property.handle)
}
// NICKNAME
if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0])
}
// ADR
for (labeledAddress in contact.addresses) {
val types = labeledAddress.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.label)
}
// NOTE
if (contact.note != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_note), null, contact.note)
}
// URL
for (labeledUrl in contact.urls) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.value)
}
// ANNIVERSARY
if (contact.anniversary != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate))
}
// BDAY
if (contact.birthDay != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate))
}
// RELATED
for (related in contact.relations) {
val types = related.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_relation), type, related.text)
}
// PHOTO
// if (contact.photo != null)
}
private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? {
if (date != null) {
val epochDate = date.time
return getDisplayedDatetime(epochDate, epochDate, true, context)
} else if (partialDate != null){
val formatter = SimpleDateFormat("d MMMM", Locale.getDefault())
val calendar = GregorianCalendar()
calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!)
calendar.set(Calendar.MONTH, partialDate.month!! - 1)
return formatter.format(calendar.time)
}
return null
}
companion object {
private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View {
val layout = parent.findViewById<View>(R.id.container) as ViewGroup
val infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false)
layout.addView(infoItem)
setTextViewText(infoItem, R.id.type, type)
setTextViewText(infoItem, R.id.title, label)
setTextViewText(infoItem, R.id.content, value)
parent.visibility = View.VISIBLE
return infoItem
}
private fun setTextViewText(parent: View, id: Int, text: String?) {
val tv = parent.findViewById<View>(id) as TextView
if (text == null) {
tv.visibility = View.GONE
} else {
tv.text = text
}
}
fun getDisplayedDatetime(startMillis: Long, endMillis: Long, allDay: Boolean, context: Context?): String? {
// Configure date/time formatting.
val flagsDate = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
var flagsTime = DateUtils.FORMAT_SHOW_TIME
if (DateFormat.is24HourFormat(context)) {
flagsTime = flagsTime or DateUtils.FORMAT_24HOUR
}
val datetimeString: String
if (allDay) {
// For multi-day allday events or single-day all-day events that are not
// today or tomorrow, use framework formatter.
// We need to remove 24hrs because full day events are from the start of a day until the start of the next
var adjustedEnd = endMillis - 24 * 60 * 60 * 1000;
if (adjustedEnd < startMillis) {
adjustedEnd = startMillis;
}
val f = Formatter(StringBuilder(50), Locale.getDefault())
datetimeString = DateUtils.formatDateRange(context, f, startMillis,
adjustedEnd, flagsDate).toString()
} else {
// For multiday events, shorten day/month names.
// Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or
DateUtils.FORMAT_ABBREV_WEEKDAY
datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis,
flagsDatetime)
}
return datetimeString
}
}
}

@ -0,0 +1,168 @@
package com.etesync.syncadapter.ui.etebase
import android.app.Dialog
import android.app.ProgressDialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.etebase.client.CollectionAccessLevel
import com.etebase.client.Utils
import com.etebase.client.exceptions.EtebaseException
import com.etebase.client.exceptions.NotFoundException
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.BaseActivity
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
class CollectionMembersFragment : Fragment() {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
private var isAdmin: Boolean = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = if (collectionModel.value!!.col.accessLevel == CollectionAccessLevel.Admin) {
isAdmin = true
inflater.inflate(R.layout.etebase_view_collection_members, container, false)
} else {
inflater.inflate(R.layout.etebase_view_collection_members_no_access, container, false)
}
if (savedInstanceState == null) {
collectionModel.observe(this) {
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.collection_members_title)
if (container != null) {
initUi(inflater, ret, it)
}
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
val meta = cachedCollection.meta
val colorSquare = v.findViewById<View>(R.id.color)
val color = LocalCalendar.parseColor(meta.color)
when (meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
colorSquare.setBackgroundColor(color)
}
Constants.ETEBASE_TYPE_TASKS -> {
colorSquare.setBackgroundColor(color)
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
colorSquare.visibility = View.GONE
}
}
val title = v.findViewById<View>(R.id.display_name) as TextView
title.text = meta.name
val desc = v.findViewById<View>(R.id.description) as TextView
desc.text = meta.description
if (isAdmin) {
v.findViewById<View>(R.id.add_member).setOnClickListener {
addMemberClicked()
}
} else {
v.findViewById<Button>(R.id.leave).setOnClickListener {
doAsync {
val membersManager = model.value!!.colMgr.getMemberManager(cachedCollection.col)
membersManager.leave()
val applicationContext = activity?.applicationContext
if (applicationContext != null) {
requestSync(applicationContext, model.value!!.account)
}
activity?.finish()
}
}
}
v.findViewById<View>(R.id.progressBar).visibility = View.GONE
}
private fun addMemberClicked() {
val view = View.inflate(requireContext(), R.layout.add_member_fragment, null)
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.collection_members_add)
.setIcon(R.drawable.ic_account_add_dark)
.setPositiveButton(android.R.string.yes) { _, _ ->
val username = view.findViewById<EditText>(R.id.username).text.toString()
val readOnly = view.findViewById<CheckBox>(R.id.read_only).isChecked
val frag = AddMemberFragment(model.value!!, collectionModel.value!!, username, if (readOnly) CollectionAccessLevel.ReadOnly else CollectionAccessLevel.ReadWrite)
frag.show(childFragmentManager, null)
}
.setNegativeButton(android.R.string.no) { _, _ -> }
dialog.setView(view)
dialog.show()
}
}
class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: CollectionAccessLevel) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(context)
progress.setTitle(R.string.collection_members_adding)
progress.setMessage(getString(R.string.please_wait))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
doAsync {
val invitationManager = accountHolder.etebase.invitationManager
try {
val profile = invitationManager.fetchUserProfile(username)
val fingerprint = Utils.prettyFingerprint(profile.pubkey)
uiThread {
val view = LayoutInflater.from(context).inflate(R.layout.fingerprint_alertdialog, null)
(view.findViewById<View>(R.id.body) as TextView).text = getString(R.string.trust_fingerprint_body, username)
(view.findViewById<View>(R.id.fingerprint) as TextView).text = fingerprint
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_fingerprint_dark)
.setTitle(R.string.trust_fingerprint_title)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
doAsync {
try {
invitationManager.invite(cachedCollection.col, username, profile.pubkey, accessLevel)
uiThread { dismiss() }
} catch (e: EtebaseException) {
uiThread { handleError(e.localizedMessage) }
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }.show()
}
} catch (e: NotFoundException) {
uiThread { handleError(getString(R.string.collection_members_error_user_not_found, username)) }
} catch (e: EtebaseException) {
uiThread { handleError(e.localizedMessage) }
}
}
return progress
}
private fun handleError(message: String) {
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.collection_members_add_error)
.setMessage(message)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
dismiss()
}
}

@ -0,0 +1,166 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.ListFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.CollectionAccessLevel
import com.etebase.client.CollectionMember
import com.etebase.client.FetchOptions
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
import java.util.concurrent.Future
class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickListener {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
private val membersModel: CollectionMembersViewModel by viewModels()
private var emptyTextView: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.collection_members_list, container, false)
//This is instead of setEmptyText() function because of Google bug
//See: https://code.google.com/p/android/issues/detail?id=21742
emptyTextView = view.findViewById<TextView>(android.R.id.empty)
return view
}
private fun setListAdapterMembers(members: List<CollectionMember>) {
val context = context
if (context != null) {
val listAdapter = MembersListAdapter(context)
setListAdapter(listAdapter)
listAdapter.addAll(members)
emptyTextView!!.setText(R.string.collection_members_list_empty)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.observe(this) {
collectionModel.observe(this) { cachedCollection ->
membersModel.loadMembers(it, cachedCollection)
}
}
membersModel.observe(this) {
setListAdapterMembers(it)
}
listView.onItemClickListener = this
}
override fun onDestroyView() {
super.onDestroyView()
membersModel.cancelLoad()
}
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val member = listAdapter?.getItem(position) as CollectionMember
if (member.accessLevel == CollectionAccessLevel.Admin) {
AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.collection_members_remove_title)
.setMessage(R.string.collection_members_remove_admin)
.setNegativeButton(android.R.string.ok) { _, _ -> }.show()
return
}
AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.collection_members_remove_title)
.setMessage(getString(R.string.collection_members_remove, member.username))
.setPositiveButton(android.R.string.yes) { dialog, which ->
membersModel.removeMember(model.value!!, collectionModel.value!!, member.username)
}
.setNegativeButton(android.R.string.no) { dialog, which -> }.show()
}
internal inner class MembersListAdapter(context: Context) : ArrayAdapter<CollectionMember>(context, R.layout.collection_members_list_item) {
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
var v = _v
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.collection_members_list_item, parent, false)
val member = getItem(position)
val tv = v!!.findViewById<View>(R.id.title) as TextView
tv.text = member!!.username
// FIXME: Also mark admins
val readOnly = v.findViewById<View>(R.id.read_only)
readOnly.visibility = if (member.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE
return v
}
}
}
class CollectionMembersViewModel : ViewModel() {
private val members = MutableLiveData<List<CollectionMember>>()
private var asyncTask: Future<Unit>? = null
fun loadMembers(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) {
asyncTask = doAsync {
val ret = LinkedList<CollectionMember>()
val col = cachedCollection.col
val memberManager = accountCollectionHolder.colMgr.getMemberManager(col)
var iterator: String? = null
var done = false
while (!done) {
val chunk = memberManager.list(FetchOptions().iterator(iterator).limit(30))
iterator = chunk.stoken
done = chunk.isDone
ret.addAll(chunk.data)
}
uiThread {
members.value = ret
}
}
}
fun removeMember(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, username: String) {
doAsync {
val col = cachedCollection.col
val memberManager = accountCollectionHolder.colMgr.getMemberManager(col)
memberManager.remove(username)
val ret = members.value!!.filter { it.username != username }
uiThread {
members.value = ret
}
}
}
fun cancelLoad() {
asyncTask?.cancel(true)
}
fun observe(owner: LifecycleOwner, observer: (List<CollectionMember>) -> Unit) =
members.observe(owner, observer)
}

@ -0,0 +1,254 @@
package com.etesync.syncadapter.ui.etebase
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import com.etebase.client.Collection
import com.etebase.client.exceptions.EtebaseException
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.BaseActivity
import org.apache.commons.lang3.StringUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import yuku.ambilwarna.AmbilWarnaDialog
class EditCollectionFragment(private val cachedCollection: CachedCollection, private val isCreating: Boolean = false) : Fragment() {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
private val itemsModel: ItemsViewModel by activityViewModels()
private val loadingModel: LoadingViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.activity_create_collection, container, false)
setHasOptionsMenu(true)
if (savedInstanceState == null) {
updateTitle()
if (container != null) {
initUi(inflater, ret)
}
}
return ret
}
fun updateTitle() {
cachedCollection.let {
var titleId: Int = R.string.create_calendar
if (isCreating) {
when (cachedCollection.meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
titleId = R.string.create_calendar
}
Constants.ETEBASE_TYPE_TASKS -> {
titleId = R.string.create_tasklist
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
titleId = R.string.create_addressbook
}
}
} else {
titleId = R.string.edit_collection
}
(activity as? BaseActivity?)?.supportActionBar?.setTitle(titleId)
}
}
private fun initUi(inflater: LayoutInflater, v: View) {
val title = v.findViewById<EditText>(R.id.display_name)
val desc = v.findViewById<EditText>(R.id.description)
val meta = cachedCollection.meta
title.setText(meta.name)
desc.setText(meta.description)
val colorSquare = v.findViewById<View>(R.id.color)
when (cachedCollection.meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
title.setHint(R.string.create_calendar_display_name_hint)
val color = LocalCalendar.parseColor(meta.color)
colorSquare.setBackgroundColor(color)
colorSquare.setOnClickListener {
AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener {
override fun onCancel(dialog: AmbilWarnaDialog) {}
override fun onOk(dialog: AmbilWarnaDialog, color: Int) {
colorSquare.setBackgroundColor(color)
}
}).show()
}
}
Constants.ETEBASE_TYPE_TASKS -> {
title.setHint(R.string.create_tasklist_display_name_hint)
val color = LocalCalendar.parseColor(meta.color)
colorSquare.setBackgroundColor(color)
colorSquare.setOnClickListener {
AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener {
override fun onCancel(dialog: AmbilWarnaDialog) {}
override fun onOk(dialog: AmbilWarnaDialog, color: Int) {
colorSquare.setBackgroundColor(color)
}
}).show()
}
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
title.setHint(R.string.create_addressbook_display_name_hint)
val colorGroup = v.findViewById<View>(R.id.color_group)
colorGroup.visibility = View.GONE
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_edit_collection, menu)
if (isCreating) {
menu.findItem(R.id.on_delete).setVisible(false)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.on_delete -> {
deleteColection()
}
R.id.on_save -> {
saveCollection()
}
}
return super.onOptionsItemSelected(item)
}
private fun deleteColection() {
val meta = cachedCollection.meta
val name = meta.name
AlertDialog.Builder(requireContext())
.setTitle(R.string.delete_collection_confirm_title)
.setMessage(getString(R.string.delete_collection_confirm_warning, name))
.setPositiveButton(android.R.string.yes) { dialog, _ ->
doDeleteCollection()
dialog.dismiss()
}
.setNegativeButton(android.R.string.no) { _, _ -> }
.show()
}
private fun doDeleteCollection() {
loadingModel.setLoading(true)
doAsync {
try {
val col = cachedCollection.col
col.delete()
uploadCollection(col)
val applicationContext = activity?.applicationContext
if (applicationContext != null) {
requestSync(applicationContext, model.value!!.account)
}
activity?.finish()
} catch (e: EtebaseException) {
uiThread {
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
} finally {
uiThread {
loadingModel.setLoading(false)
}
}
}
}
private fun saveCollection() {
var ok = true
val meta = cachedCollection.meta
val v = requireView()
var edit = v.findViewById<EditText>(R.id.display_name)
meta.name = edit.text.toString()
if (TextUtils.isEmpty(meta.name)) {
edit.error = getString(R.string.create_collection_display_name_required)
ok = false
}
edit = v.findViewById<EditText>(R.id.description)
meta.description = StringUtils.trimToNull(edit.text.toString())
if (ok) {
when (meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> {
val view = v.findViewById<View>(R.id.color)
val color = (view.background as ColorDrawable).color
meta.color = String.format("#%06X", 0xFFFFFF and color)
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
}
}
loadingModel.setLoading(true)
doAsync {
try {
val col = cachedCollection.col
col.meta = meta
uploadCollection(col)
val applicationContext = activity?.applicationContext
if (applicationContext != null) {
requestSync(applicationContext, model.value!!.account)
}
if (isCreating) {
// Load the items since we just created it
itemsModel.loadItems(model.value!!, cachedCollection)
parentFragmentManager.commit {
replace(R.id.fragment_container, ViewCollectionFragment())
}
} else {
parentFragmentManager.popBackStack()
}
} catch (e: EtebaseException) {
uiThread {
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
} finally {
uiThread {
loadingModel.setLoading(false)
}
}
}
}
}
private fun uploadCollection(col: Collection) {
val accountHolder = model.value!!
val etebaseLocalCache = accountHolder.etebaseLocalCache
val colMgr = accountHolder.colMgr
colMgr.upload(col)
synchronized(etebaseLocalCache) {
etebaseLocalCache.collectionSet(colMgr, col)
}
collectionModel.loadCollection(model.value!!, col.uid)
}
}

@ -0,0 +1,79 @@
package com.etesync.syncadapter.ui.etebase
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.BaseActivity
import com.etesync.syncadapter.ui.importlocal.ImportFragment
import com.etesync.syncadapter.ui.importlocal.LocalCalendarImportFragment
import com.etesync.syncadapter.ui.importlocal.LocalContactImportFragment
class ImportCollectionFragment : Fragment() {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.import_actions_list, container, false)
setHasOptionsMenu(true)
if (savedInstanceState == null) {
collectionModel.observe(this) {
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_dialog_title)
if (container != null) {
initUi(inflater, ret, it)
}
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
val accountHolder = model.value!!
var card = v.findViewById<View>(R.id.import_file)
var img = card.findViewById<View>(R.id.action_icon) as ImageView
var text = card.findViewById<View>(R.id.action_text) as TextView
img.setImageResource(R.drawable.ic_file_white)
text.setText(R.string.import_button_file)
card.setOnClickListener {
parentFragmentManager.commit {
add(ImportFragment.newInstance(accountHolder.account, cachedCollection), null)
}
}
card = v.findViewById(R.id.import_account)
img = card.findViewById<View>(R.id.action_icon) as ImageView
text = card.findViewById<View>(R.id.action_text) as TextView
img.setImageResource(R.drawable.ic_account_circle_white)
text.setText(R.string.import_button_local)
card.setOnClickListener {
if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR) {
parentFragmentManager.commit {
replace(R.id.fragment_container, LocalCalendarImportFragment(accountHolder.account, cachedCollection.col.uid))
addToBackStack(null)
}
} else if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK) {
parentFragmentManager.commit {
replace(R.id.fragment_container, LocalContactImportFragment(accountHolder.account, cachedCollection.col.uid))
addToBackStack(null)
}
}
// FIXME: should be in the fragments once we kill legacy
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_select_account)
}
if (collectionModel.value!!.meta.collectionType == Constants.ETEBASE_TYPE_TASKS) {
card.visibility = View.GONE
}
}
}

@ -0,0 +1,43 @@
package com.etesync.syncadapter.ui.etebase
import android.accounts.Account
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.commit
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.BaseActivity
class InvitationsActivity : BaseActivity() {
private lateinit var account: Account
private val model: AccountViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
setContentView(R.layout.etebase_collection_activity)
if (savedInstanceState == null) {
model.loadAccount(this, account)
title = getString(R.string.invitations_title)
supportFragmentManager.commit {
replace(R.id.fragment_container, InvitationsListFragment())
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
companion object {
private val EXTRA_ACCOUNT = "account"
fun newIntent(context: Context, account: Account): Intent {
val intent = Intent(context, InvitationsActivity::class.java)
intent.putExtra(EXTRA_ACCOUNT, account)
return intent
}
}
}

@ -0,0 +1,172 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.ListFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.CollectionAccessLevel
import com.etebase.client.FetchOptions
import com.etebase.client.SignedInvitation
import com.etebase.client.Utils
import com.etesync.syncadapter.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
import java.util.concurrent.Future
class InvitationsListFragment : ListFragment(), AdapterView.OnItemClickListener {
private val model: AccountViewModel by activityViewModels()
private val invitationsModel: InvitationsViewModel by viewModels()
private var emptyTextView: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.invitations_list, container, false)
//This is instead of setEmptyText() function because of Google bug
//See: https://code.google.com/p/android/issues/detail?id=21742
emptyTextView = view.findViewById<TextView>(android.R.id.empty)
return view
}
private fun setListAdapterInvitations(invitations: List<SignedInvitation>) {
val context = context
if (context != null) {
val listAdapter = InvitationsListAdapter(context)
setListAdapter(listAdapter)
listAdapter.addAll(invitations)
emptyTextView!!.setText(R.string.invitations_list_empty)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.observe(this) {
invitationsModel.loadInvitations(it)
}
invitationsModel.observe(this) {
setListAdapterInvitations(it)
}
listView.onItemClickListener = this
}
override fun onDestroyView() {
super.onDestroyView()
invitationsModel.cancelLoad()
}
override fun onItemClick(parent: AdapterView<*>, view_: View, position: Int, id: Long) {
val invitation = listAdapter?.getItem(position) as SignedInvitation
val fingerprint = Utils.prettyFingerprint(invitation.fromPubkey)
val view = layoutInflater.inflate(R.layout.invitation_alert_dialog, null)
view.findViewById<TextView>(R.id.body).text = getString(R.string.invitations_accept_reject_dialog)
view.findViewById<TextView>(R.id.fingerprint).text = fingerprint
AlertDialog.Builder(requireContext())
.setTitle(R.string.invitations_title)
.setIcon(R.drawable.ic_email_black)
.setView(view)
.setNegativeButton(R.string.invitations_reject) { dialogInterface, i ->
invitationsModel.reject(model.value!!, invitation)
}
.setPositiveButton(R.string.invitations_accept) { dialogInterface, i ->
invitationsModel.accept(model.value!!, invitation)
}
.show()
return
}
internal inner class InvitationsListAdapter(context: Context) : ArrayAdapter<SignedInvitation>(context, R.layout.invitations_list_item) {
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
var v = _v
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.invitations_list_item, parent, false)
val invitation = getItem(position)!!
val tv = v!!.findViewById<View>(R.id.title) as TextView
// FIXME: Should have a sensible string here
tv.text = "Invitation ${position}"
// FIXME: Also mark admins
val readOnly = v.findViewById<View>(R.id.read_only)
readOnly.visibility = if (invitation.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE
return v
}
}
}
class InvitationsViewModel : ViewModel() {
private val invitations = MutableLiveData<List<SignedInvitation>>()
private var asyncTask: Future<Unit>? = null
fun loadInvitations(accountCollectionHolder: AccountHolder) {
asyncTask = doAsync {
val ret = LinkedList<SignedInvitation>()
val invitationManager = accountCollectionHolder.etebase.invitationManager
var iterator: String? = null
var done = false
while (!done) {
val chunk = invitationManager.listIncoming(FetchOptions().iterator(iterator).limit(30))
iterator = chunk.stoken
done = chunk.isDone
ret.addAll(chunk.data)
}
uiThread {
invitations.value = ret
}
}
}
fun accept(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
doAsync {
val invitationManager = accountCollectionHolder.etebase.invitationManager
invitationManager.accept(invitation)
val ret = invitations.value!!.filter { it != invitation }
uiThread {
invitations.value = ret
}
}
}
fun reject(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
doAsync {
val invitationManager = accountCollectionHolder.etebase.invitationManager
invitationManager.reject(invitation)
val ret = invitations.value!!.filter { it != invitation }
uiThread {
invitations.value = ret
}
}
}
fun cancelLoad() {
asyncTask?.cancel(true)
}
fun observe(owner: LifecycleOwner, observer: (List<SignedInvitation>) -> Unit) =
invitations.observe(owner, observer)
}

@ -0,0 +1,147 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.fragment.app.ListFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.FetchOptions
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.CachedItem
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.etebase.ListEntriesFragment.Companion.setItemView
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
import java.util.concurrent.Future
class ItemRevisionsListFragment(private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : ListFragment(), AdapterView.OnItemClickListener {
private val model: AccountViewModel by activityViewModels()
private val revisionsModel: RevisionsViewModel by viewModels()
private var state: Parcelable? = null
private var emptyTextView: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.journal_viewer_list, container, false)
//This is instead of setEmptyText() function because of Google bug
//See: https://code.google.com/p/android/issues/detail?id=21742
emptyTextView = view.findViewById<View>(android.R.id.empty) as TextView
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var restored = false
revisionsModel.loadRevisions(model.value!!, cachedCollection, cachedItem)
revisionsModel.observe(this) {
val entries = it.sortedByDescending { item ->
item.meta.mtime ?: 0
}
val listAdapter = EntriesListAdapter(requireContext(), cachedCollection)
setListAdapter(listAdapter)
listAdapter.addAll(entries)
if(!restored && (state != null)) {
listView.onRestoreInstanceState(state)
restored = true
}
emptyTextView!!.text = getString(R.string.journal_entries_list_empty)
}
listView.onItemClickListener = this
}
override fun onPause() {
state = listView.onSaveInstanceState()
super.onPause()
}
override fun onDestroyView() {
super.onDestroyView()
revisionsModel.cancelLoad()
}
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val item = listAdapter?.getItem(position) as CachedItem
activity?.supportFragmentManager?.commit {
replace(R.id.fragment_container, CollectionItemFragment(item))
addToBackStack(null)
}
}
internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter<CachedItem>(context, R.layout.journal_viewer_list_item) {
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
var v = _v
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
val item = getItem(position)
setItemView(v, cachedCollection.meta.collectionType, item)
/* FIXME: handle entry error:
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()
if (entryError != null) {
val errorIcon = v.findViewById<View>(R.id.error) as ImageView
errorIcon.visibility = View.VISIBLE
}
*/
return v
}
}
}
class RevisionsViewModel : ViewModel() {
private val revisions = MutableLiveData<List<CachedItem>>()
private var asyncTask: Future<Unit>? = null
fun loadRevisions(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, cachedItem: CachedItem) {
asyncTask = doAsync {
val ret = LinkedList<CachedItem>()
val col = cachedCollection.col
val itemManager = accountCollectionHolder.colMgr.getItemManager(col)
var iterator: String? = null
var done = false
while (!done) {
val chunk = itemManager.itemRevisions(cachedItem.item, FetchOptions().iterator(iterator).limit(30))
iterator = chunk.iterator
done = chunk.isDone
ret.addAll(chunk.data.map { CachedItem(it, it.meta, it.contentString) })
}
uiThread {
revisions.value = ret
}
}
}
fun cancelLoad() {
asyncTask?.cancel(true)
}
fun observe(owner: LifecycleOwner, observer: (List<CachedItem>) -> Unit) =
revisions.observe(owner, observer)
}

@ -0,0 +1,152 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.ListFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.CachedItem
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import java.text.SimpleDateFormat
class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
private val collectionModel: CollectionViewModel by activityViewModels()
private val itemsModel: ItemsViewModel by activityViewModels()
private var state: Parcelable? = null
private var emptyTextView: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.journal_viewer_list, container, false)
//This is instead of setEmptyText() function because of Google bug
//See: https://code.google.com/p/android/issues/detail?id=21742
emptyTextView = view.findViewById<View>(android.R.id.empty) as TextView
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var restored = false
collectionModel.observe(this) { col ->
itemsModel.observe(this) {
val entries = it.sortedByDescending { item ->
item.meta.mtime ?: 0
}
val listAdapter = EntriesListAdapter(requireContext(), col)
setListAdapter(listAdapter)
listAdapter.addAll(entries)
if(!restored && (state != null)) {
listView.onRestoreInstanceState(state)
restored = true
}
emptyTextView!!.text = getString(R.string.journal_entries_list_empty)
}
}
listView.onItemClickListener = this
}
override fun onPause() {
state = listView.onSaveInstanceState()
super.onPause()
}
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val item = listAdapter?.getItem(position) as CachedItem
activity?.supportFragmentManager?.commit {
replace(R.id.fragment_container, CollectionItemFragment(item))
addToBackStack(EditCollectionFragment::class.java.name)
}
}
internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter<CachedItem>(context, R.layout.journal_viewer_list_item) {
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
var v = _v
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
val item = getItem(position)
setItemView(v, cachedCollection.meta.collectionType, item)
/* FIXME: handle entry error:
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()
if (entryError != null) {
val errorIcon = v.findViewById<View>(R.id.error) as ImageView
errorIcon.visibility = View.VISIBLE
}
*/
return v
}
}
companion object {
private val dateFormatter = SimpleDateFormat()
private fun getLine(content: String?, prefix: String): String? {
var content: String? = content ?: return null
val start = content!!.indexOf(prefix)
if (start >= 0) {
val end = content.indexOf("\n", start)
content = content.substring(start + prefix.length, end)
} else {
content = null
}
return content
}
fun setItemView(v: View, collectionType: String, item: CachedItem) {
var tv = v.findViewById<View>(R.id.title) as TextView
// FIXME: hacky way to make it show sensible info
val prefix: String = when (collectionType) {
Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> {
"SUMMARY:"
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
"FN:"
}
else -> {
""
}
}
val fullContent = item.content
var content = getLine(fullContent, prefix)
content = content ?: "Not found"
tv.text = content
tv = v.findViewById<View>(R.id.description) as TextView
// FIXME: Don't use a hard-coded string
content = "Modified: ${dateFormatter.format(item.meta.mtime ?: 0)}"
tv.text = content
val action = v.findViewById<View>(R.id.action) as ImageView
if (item.item.isDeleted) {
action.setImageResource(R.drawable.action_delete)
} else {
action.setImageResource(R.drawable.action_change)
}
}
}
}

@ -0,0 +1,226 @@
/*
* Copyright © 2013 2016 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 com.etesync.syncadapter.ui.etebase
import android.app.Dialog
import android.app.ProgressDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckedTextView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.Account
import com.etebase.client.Client
import com.etebase.client.User
import com.etebase.client.exceptions.EtebaseException
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder
import com.etesync.syncadapter.ui.setup.CreateAccountFragment
import com.etesync.syncadapter.ui.setup.DetectConfigurationFragment
import com.etesync.syncadapter.ui.setup.LoginCredentialsFragment
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import net.cachapa.expandablelayout.ExpandableLayout
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.net.URI
import java.util.concurrent.Future
class SignupFragment(private val initialUsername: String?, private val initialPassword: String?) : Fragment() {
internal lateinit var editUserName: TextInputLayout
internal lateinit var editEmail: TextInputLayout
internal lateinit var editPassword: TextInputLayout
internal lateinit var showAdvanced: CheckedTextView
internal lateinit var customServer: TextInputEditText
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.signup_fragment, container, false)
editUserName = v.findViewById(R.id.user_name)
editEmail = v.findViewById(R.id.email)
editPassword = v.findViewById(R.id.url_password)
showAdvanced = v.findViewById(R.id.show_advanced)
customServer = v.findViewById(R.id.custom_server)
if (savedInstanceState == null) {
editUserName.editText?.setText(initialUsername ?: "")
editPassword.editText?.setText(initialPassword ?: "")
}
val login = v.findViewById<Button>(R.id.login)
login.setOnClickListener {
parentFragmentManager.commit {
replace(android.R.id.content, LoginCredentialsFragment(editUserName.editText?.text.toString(), editPassword.editText?.text.toString()))
}
}
val createAccount = v.findViewById<Button>(R.id.create_account)
createAccount.setOnClickListener {
val credentials = validateData()
if (credentials != null) {
SignupDoFragment(credentials).show(fragmentManager!!, null)
}
}
val advancedLayout = v.findViewById<View>(R.id.advanced_layout) as ExpandableLayout
showAdvanced.setOnClickListener {
if (showAdvanced.isChecked) {
showAdvanced.isChecked = false
advancedLayout.collapse()
} else {
showAdvanced.isChecked = true
advancedLayout.expand()
}
}
return v
}
protected fun validateData(): SignupCredentials? {
var valid = true
val userName = editUserName.editText?.text.toString()
if (userName.isEmpty()) {
editUserName.error = getString(R.string.login_username_error)
valid = false
} else {
editUserName.error = null
}
val email = editEmail.editText?.text.toString()
if (email.isEmpty()) {
editEmail.error = getString(R.string.login_email_address_error)
valid = false
} else {
editEmail.error = null
}
val password = editPassword.editText?.text.toString()
if (password.isEmpty()) {
editPassword.error = getString(R.string.signup_password_restrictions)
valid = false
} else {
editPassword.error = null
}
var uri: URI? = null
if (showAdvanced.isChecked) {
val server = customServer.text.toString()
// If this field is null, just use the default
if (!server.isEmpty()) {
val url = server.toHttpUrlOrNull()
if (url != null) {
uri = url.toUri()
customServer.error = null
} else {
customServer.error = getString(R.string.login_custom_server_error)
valid = false
}
}
}
return if (valid) SignupCredentials(uri, userName, email, password) else null
}
}
class SignupDoFragment(private val signupCredentials: SignupCredentials) : DialogFragment() {
private val model: ConfigurationViewModel by viewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
return progress
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
model.signup(requireContext(), signupCredentials)
model.observe(this) {
if (it.isFailed) {
// no service found: show error message
requireFragmentManager().beginTransaction()
.add(DetectConfigurationFragment.NothingDetectedFragment.newInstance(it.error!!.localizedMessage), null)
.commitAllowingStateLoss()
} else {
requireFragmentManager().beginTransaction()
.replace(android.R.id.content, CreateAccountFragment.newInstance(it))
.addToBackStack(null)
.commitAllowingStateLoss()
}
dismissAllowingStateLoss()
}
}
}
}
class ConfigurationViewModel : ViewModel() {
private val account = MutableLiveData<BaseConfigurationFinder.Configuration>()
private var asyncTask: Future<Unit>? = null
fun signup(context: Context, credentials: SignupCredentials) {
asyncTask = doAsync {
val httpClient = HttpClient.Builder(context).build().okHttpClient
val uri = credentials.uri ?: URI(Constants.etebaseServiceUrl)
var etebaseSession: String? = null
var exception: Throwable? = null
try {
val client = Client.create(httpClient, uri.toString())
val user = User(credentials.userName, credentials.email)
val etebase = Account.signup(client, user, credentials.password)
etebaseSession = etebase.save(null)
} catch (e: EtebaseException) {
exception = e
}
uiThread {
account.value = BaseConfigurationFinder.Configuration(
uri,
credentials.userName,
etebaseSession,
null,
null,
exception
)
}
}
}
fun cancelLoad() {
asyncTask?.cancel(true)
}
fun observe(owner: LifecycleOwner, observer: (BaseConfigurationFinder.Configuration) -> Unit) =
account.observe(owner, observer)
}
data class SignupCredentials(val uri: URI?, val userName: String, val email: String, val password: String)

@ -0,0 +1,157 @@
package com.etesync.syncadapter.ui.etebase
import android.content.DialogInterface
import android.os.Bundle
import android.view.*
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import com.etebase.client.CollectionAccessLevel
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.ui.BaseActivity
import com.etesync.syncadapter.ui.WebViewActivity
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import tourguide.tourguide.ToolTip
import java.util.*
class ViewCollectionFragment : Fragment() {
private val collectionModel: CollectionViewModel by activityViewModels()
private val itemsModel: ItemsViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.view_collection_fragment, container, false)
setHasOptionsMenu(true)
if (savedInstanceState == null) {
collectionModel.observe(this) {
(activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name
if (container != null) {
initUi(inflater, ret, it)
}
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, container: View, cachedCollection: CachedCollection) {
val title = container.findViewById<TextView>(R.id.display_name)
if (!HintManager.getHintSeen(requireContext(), HINT_IMPORT)) {
val tourGuide = ShowcaseBuilder.getBuilder(requireActivity())
.setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_import)).setGravity(Gravity.BOTTOM))
.setPointer(null)
tourGuide.mOverlay.setHoleRadius(0)
tourGuide.playOn(title)
HintManager.setHintSeen(requireContext(), HINT_IMPORT, true)
}
val fab = container.findViewById<FloatingActionButton>(R.id.fab)
fab?.setOnClickListener {
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.use_native_apps_title)
.setMessage(R.string.use_native_apps_body)
.setNegativeButton(R.string.navigation_drawer_guide, { _: DialogInterface, _: Int -> WebViewActivity.openUrl(requireContext(), Constants.helpUri) })
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
val col = cachedCollection.col
val meta = cachedCollection.meta
val isAdmin = col.accessLevel == CollectionAccessLevel.Admin
val colorSquare = container.findViewById<View>(R.id.color)
val color = LocalCalendar.parseColor(meta.color)
when (meta.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
colorSquare.setBackgroundColor(color)
}
Constants.ETEBASE_TYPE_TASKS -> {
colorSquare.setBackgroundColor(color)
val tasksNotShowing = container.findViewById<View>(R.id.tasks_not_showing)
tasksNotShowing.visibility = View.VISIBLE
}
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
colorSquare.visibility = View.GONE
}
}
title.text = meta.name
val desc = container.findViewById<TextView>(R.id.description)
desc.text = meta.description
val owner = container.findViewById<TextView>(R.id.owner)
if (isAdmin) {
owner.visibility = View.GONE
} else {
owner.visibility = View.VISIBLE
owner.text = "Shared with us" // FIXME: Figure out how to represent it and don't use a hardcoded string
}
itemsModel.observe(this) {
val stats = container.findViewById<TextView>(R.id.stats)
container.findViewById<View>(R.id.progressBar).visibility = View.GONE
stats.text = String.format(Locale.getDefault(), "Change log items: %d", it.size)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_view_collection, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val cachedCollection = collectionModel.value!!
when (item.itemId) {
R.id.on_edit -> {
if (cachedCollection.col.accessLevel == CollectionAccessLevel.Admin) {
parentFragmentManager.commit {
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection))
addToBackStack(EditCollectionFragment::class.java.name)
}
} else {
val dialog = AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title)
.setMessage(R.string.edit_owner_only_anon)
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
dialog.show()
}
}
R.id.on_manage_members -> {
parentFragmentManager.commit {
replace(R.id.fragment_container, CollectionMembersFragment())
addToBackStack(null)
}
}
R.id.on_import -> {
if (cachedCollection.col.accessLevel != CollectionAccessLevel.ReadOnly) {
parentFragmentManager.commit {
replace(R.id.fragment_container, ImportCollectionFragment())
addToBackStack(null)
}
} else {
val dialog = AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title)
.setMessage(R.string.edit_owner_only_anon)
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
dialog.show()
}
}
}
return super.onOptionsItemSelected(item)
}
companion object {
private val HINT_IMPORT = "Import"
}
}

@ -14,7 +14,7 @@ import com.etesync.syncadapter.R
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.ui.BaseActivity
class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface {
class ImportActivity : BaseActivity(), SelectImportMethod, DialogInterface {
private lateinit var account: Account
protected lateinit var info: CollectionInfo
@ -83,13 +83,6 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo
return super.onKeyDown(keyCode, event)
}
override fun onImportResult(importResult: ResultFragment.ImportResult) {
val fragment = ResultFragment.newInstance(importResult)
supportFragmentManager.beginTransaction()
.add(fragment, "importResult")
.commitAllowingStateLoss()
}
override fun cancel() {
finish()
}
@ -108,9 +101,9 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mSelectImportMethod = activity as SelectImportMethod?
mSelectImportMethod = activity as SelectImportMethod
} catch (e: ClassCastException) {
throw ClassCastException(activity!!.toString() + " must implement MyInterface ")
throw ClassCastException(activity.toString() + " must implement MyInterface ")
}
}

@ -14,13 +14,13 @@ import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.commit
import at.bitfire.ical4android.*
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.Constants.*
import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
@ -35,19 +35,14 @@ import java.io.InputStream
import java.io.InputStreamReader
class ImportFragment : DialogFragment() {
class ImportFragment(private val account: Account, private val uid: String, private val enumType: CollectionInfo.Type) : DialogFragment() {
private lateinit var account: Account
private lateinit var info: CollectionInfo
private var inputStream: InputStream? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isCancelable = false
retainInstance = true
account = arguments!!.getParcelable(KEY_ACCOUNT)!!
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -57,7 +52,7 @@ class ImportFragment : DialogFragment() {
} else {
val data = ImportResult()
data.e = Exception(getString(R.string.import_permission_required))
(activity as ResultFragment.OnImportCallback).onImportResult(data)
onImportResult(data)
dismissAllowingStateLoss()
}
@ -118,7 +113,7 @@ class ImportFragment : DialogFragment() {
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.action = Intent.ACTION_GET_CONTENT
when (info.enumType) {
when (enumType) {
CollectionInfo.Type.CALENDAR -> intent.type = "text/calendar"
CollectionInfo.Type.TASKS -> intent.type = "text/calendar"
CollectionInfo.Type.ADDRESS_BOOK -> intent.type = "text/x-vcard"
@ -132,7 +127,7 @@ class ImportFragment : DialogFragment() {
val data = ImportResult()
data.e = Exception("Failed to open file chooser.\nPlease install one.")
(activity as ResultFragment.OnImportCallback).onImportResult(data)
onImportResult(data)
dismissAllowingStateLoss()
}
@ -146,7 +141,7 @@ class ImportFragment : DialogFragment() {
if (data != null) {
// Get the URI of the selected file
val uri = data.data!!
Logger.log.info("Starting import into ${info.uid} from file ${uri}")
Logger.log.info("Starting import into ${uid} from file ${uri}")
try {
inputStream = activity!!.contentResolver.openInputStream(uri)
@ -157,7 +152,7 @@ class ImportFragment : DialogFragment() {
val importResult = ImportResult()
importResult.e = e
(activity as ResultFragment.OnImportCallback).onImportResult(importResult)
onImportResult(importResult)
dismissAllowingStateLoss()
}
@ -172,7 +167,7 @@ class ImportFragment : DialogFragment() {
}
fun loadFinished(data: ImportResult) {
(activity as ResultFragment.OnImportCallback).onImportResult(data)
onImportResult(data)
Logger.log.info("Finished import")
@ -217,7 +212,7 @@ class ImportFragment : DialogFragment() {
val context = context!!
val importReader = InputStreamReader(inputStream)
if (info.enumType == CollectionInfo.Type.CALENDAR) {
if (enumType == CollectionInfo.Type.CALENDAR) {
val events = Event.eventsFromReader(importReader, null)
importReader.close()
@ -239,7 +234,7 @@ class ImportFragment : DialogFragment() {
val localCalendar: LocalCalendar?
try {
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, uid!!)
if (localCalendar == null) {
throw FileNotFoundException("Failed to load local resource.")
}
@ -270,7 +265,7 @@ class ImportFragment : DialogFragment() {
entryProcessed()
}
} else if (info.enumType == CollectionInfo.Type.TASKS) {
} else if (enumType == CollectionInfo.Type.TASKS) {
val tasks = Task.tasksFromReader(importReader)
importReader.close()
@ -297,7 +292,7 @@ class ImportFragment : DialogFragment() {
provider?.let {
val localTaskList: LocalTaskList?
try {
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, info.uid!!)
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, uid!!)
if (localTaskList == null) {
throw FileNotFoundException("Failed to load local resource.")
}
@ -325,7 +320,7 @@ class ImportFragment : DialogFragment() {
entryProcessed()
}
}
} else if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
} else if (enumType == CollectionInfo.Type.ADDRESS_BOOK) {
val uidToLocalId = HashMap<String?, Long>()
val downloader = ContactsSyncManager.ResourceDownloader(context)
val contacts = Contact.fromReader(importReader, downloader)
@ -346,7 +341,7 @@ class ImportFragment : DialogFragment() {
return result
}
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, info.uid!!)
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, uid!!)
if (localAddressBook == null) {
throw FileNotFoundException("Failed to load local address book.")
}
@ -424,18 +419,30 @@ class ImportFragment : DialogFragment() {
}
}
fun onImportResult(importResult: ImportResult) {
val fragment = ResultFragment.newInstance(importResult)
parentFragmentManager.commit(true) {
add(fragment, "importResult")
}
}
companion object {
private val REQUEST_CODE = 6384 // onActivityResult request
private val TAG_PROGRESS_MAX = "progressMax"
fun newInstance(account: Account, info: CollectionInfo): ImportFragment {
val frag = ImportFragment()
val args = Bundle(1)
args.putParcelable(KEY_ACCOUNT, account)
args.putSerializable(KEY_COLLECTION_INFO, info)
frag.arguments = args
return frag
return ImportFragment(account, info.uid!!, info.enumType!!)
}
fun newInstance(account: Account, cachedCollection: CachedCollection): ImportFragment {
val enumType = when (cachedCollection.meta.collectionType) {
ETEBASE_TYPE_CALENDAR -> CollectionInfo.Type.CALENDAR
ETEBASE_TYPE_TASKS -> CollectionInfo.Type.TASKS
ETEBASE_TYPE_ADDRESS_BOOK -> CollectionInfo.Type.ADDRESS_BOOK
else -> throw Exception("Got unsupported collection type")
}
return ImportFragment(account, cachedCollection.col.uid, enumType)
}
}
}

@ -14,9 +14,8 @@ import android.widget.ExpandableListView
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.ListFragment
import androidx.fragment.app.commit
import at.bitfire.ical4android.CalendarStorageException
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
@ -24,17 +23,10 @@ import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.resource.LocalEvent
class LocalCalendarImportFragment : ListFragment() {
private lateinit var account: Account
private lateinit var info: CollectionInfo
class LocalCalendarImportFragment(private val account: Account, private val uid: String) : ListFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
account = arguments!!.getParcelable(KEY_ACCOUNT)!!
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -200,7 +192,7 @@ class LocalCalendarImportFragment : ListFragment() {
if (progressDialog.isShowing && !activity.isDestroyed) {
progressDialog.dismiss()
}
(activity as ResultFragment.OnImportCallback).onImportResult(result)
onImportResult(result)
}
private fun importEvents(fromCalendar: LocalCalendar): ResultFragment.ImportResult {
@ -208,7 +200,7 @@ class LocalCalendarImportFragment : ListFragment() {
try {
val localCalendar = LocalCalendar.findByName(account,
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
LocalCalendar.Factory, info!!.uid!!)
LocalCalendar.Factory, uid)
val localEvents = fromCalendar.findAll()
val total = localEvents.size
progressDialog.max = total
@ -248,15 +240,17 @@ class LocalCalendarImportFragment : ListFragment() {
}
}
fun onImportResult(importResult: ResultFragment.ImportResult) {
val fragment = ResultFragment.newInstance(importResult)
parentFragmentManager.commit(true) {
add(fragment, "importResult")
}
}
companion object {
fun newInstance(account: Account, info: CollectionInfo): LocalCalendarImportFragment {
val frag = LocalCalendarImportFragment()
val args = Bundle(1)
args.putParcelable(KEY_ACCOUNT, account)
args.putSerializable(KEY_COLLECTION_INFO, info)
frag.arguments = args
return frag
return LocalCalendarImportFragment(account, info.uid!!)
}
}
}

@ -18,11 +18,10 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
@ -32,18 +31,12 @@ import com.etesync.syncadapter.resource.LocalGroup
import java.util.*
class LocalContactImportFragment : Fragment() {
private lateinit var account: Account
private lateinit var info: CollectionInfo
class LocalContactImportFragment(private val account: Account, private val uid: String) : Fragment() {
private var recyclerView: RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
account = arguments!!.getParcelable(KEY_ACCOUNT)
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -134,15 +127,16 @@ class LocalContactImportFragment : Fragment() {
if (progressDialog.isShowing && !activity.isDestroyed) {
progressDialog.dismiss()
}
(activity as ResultFragment.OnImportCallback).onImportResult(result)
onImportResult(result)
}
private fun importContacts(localAddressBook: LocalAddressBook): ResultFragment.ImportResult {
val result = ResultFragment.ImportResult()
try {
val addressBook = LocalAddressBook.findByUid(context!!,
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
account, info.uid!!)!!
val addressBook = LocalAddressBook.findByUid(requireContext(),
requireContext().contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
account, uid)
?: throw Exception("Could not find address book")
val localContacts = localAddressBook.findAllContacts()
val localGroups = localAddressBook.findAllGroups()
val oldIdToNewId = HashMap<Long, Long>()
@ -214,6 +208,13 @@ class LocalContactImportFragment : Fragment() {
}
}
fun onImportResult(importResult: ResultFragment.ImportResult) {
val fragment = ResultFragment.newInstance(importResult)
parentFragmentManager.commit(true) {
add(fragment, "importResult")
}
}
class ImportContactAdapter
/**
* Initialize the dataset of the Adapter.
@ -316,13 +317,7 @@ class LocalContactImportFragment : Fragment() {
companion object {
fun newInstance(account: Account, info: CollectionInfo): LocalContactImportFragment {
val frag = LocalContactImportFragment()
val args = Bundle(1)
args.putParcelable(KEY_ACCOUNT, account)
args.putSerializable(KEY_COLLECTION_INFO, info)
frag.arguments = args
return frag
return LocalContactImportFragment(account, info.uid!!)
}
}
}

@ -19,7 +19,7 @@ class ResultFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
result = arguments!!.getSerializable(KEY_RESULT) as ImportResult
result = requireArguments().getSerializable(KEY_RESULT) as ImportResult
}
override fun onDismiss(dialog: DialogInterface) {
@ -32,7 +32,7 @@ class ResultFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
if (result!!.isFailed) {
return AlertDialog.Builder(activity!!)
return AlertDialog.Builder(requireActivity())
.setTitle(R.string.import_dialog_failed_title)
.setIcon(R.drawable.ic_error_dark)
.setMessage(getString(R.string.import_dialog_failed_body, result!!.e!!.localizedMessage))
@ -72,10 +72,6 @@ class ResultFragment : DialogFragment() {
}
}
interface OnImportCallback {
fun onImportResult(importResult: ImportResult)
}
companion object {
private val KEY_RESULT = "result"

@ -88,7 +88,7 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val entry = listAdapter?.getItem(position) as EntryEntity
startActivity(JournalItemActivity.newIntent(context!!, account, info, entry.content))
startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content))
}
internal inner class EntriesListAdapter(context: Context) : ArrayAdapter<EntryEntity>(context, R.layout.journal_viewer_list_item) {

@ -8,16 +8,21 @@
package com.etesync.syncadapter.ui.setup
import android.content.Context
import com.etebase.client.Account
import com.etebase.client.Client
import com.etebase.client.exceptions.EtebaseException
import com.etesync.syncadapter.HttpClient
import com.etesync.journalmanager.Crypto
import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.JournalAuthenticator
import com.etesync.journalmanager.UserInfoManager
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.io.Serializable
import java.net.URI
@ -30,45 +35,102 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
httpClient = HttpClient.Builder(context).build().okHttpClient
}
private fun isServerEtebase(): Boolean {
if (credentials.uri != null) {
val remote = credentials.uri.toHttpUrlOrNull()!!.newBuilder()
.addPathSegments("api/v1/authentication/is_etebase/")
.build()
fun findInitialConfiguration(): Configuration {
val request = Request.Builder()
.get()
.url(remote)
.build()
val response = httpClient.newCall(request).execute()
return response.isSuccessful
} else {
return !credentials.userName.contains("@")
}
}
fun findInitialConfigurationLegacy(): Configuration {
var exception: Throwable? = null
val cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK)
val calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR)
val authenticator = JournalAuthenticator(httpClient, credentials.uri?.toHttpUrlOrNull()!!)
val uri = credentials.uri ?: URI(Constants.serviceUrl.toString())
val authenticator = JournalAuthenticator(httpClient, uri.toHttpUrlOrNull()!!)
var authtoken: String? = null
var userInfo: UserInfoManager.UserInfo? = null
try {
authtoken = authenticator.getAuthToken(credentials.userName, credentials.password)
val authenticatedHttpClient = HttpClient.Builder(context, credentials.uri.host, authtoken!!).build().okHttpClient
val userInfoManager = UserInfoManager(authenticatedHttpClient, credentials.uri.toHttpUrlOrNull()!!)
val authenticatedHttpClient = HttpClient.Builder(context, uri.host, authtoken!!).build().okHttpClient
val userInfoManager = UserInfoManager(authenticatedHttpClient, uri.toHttpUrlOrNull()!!)
userInfo = userInfoManager.fetch(credentials.userName)
} catch (e: Exceptions.HttpException) {
Logger.log.warning(e.message)
Logger.log.warning(e.localizedMessage)
exception = e
} catch (e: IOException) {
Logger.log.warning(e.message)
Logger.log.warning(e.localizedMessage)
exception = e
}
return Configuration(
credentials.uri,
credentials.userName, authtoken,
cardDavConfig, calDavConfig,
uri,
credentials.userName,
null,
authtoken,
userInfo,
exception
)
}
protected fun findInitialConfiguration(service: CollectionInfo.Type): Configuration.ServiceInfo {
// put discovered information here
val config = Configuration.ServiceInfo()
Logger.log.info("Finding initial " + service.toString() + " service configuration")
fun findInitialConfigurationEtebase(): Configuration {
var exception: Throwable? = null
val uri = credentials.uri ?: URI(Constants.etebaseServiceUrl)
return config
var etebaseSession: String? = null
try {
val client = Client.create(httpClient, uri.toString())
val etebase = Account.login(client, credentials.userName, credentials.password)
etebaseSession = etebase.save(null)
} catch (e: java.lang.Exception) {
Logger.log.warning(e.localizedMessage)
exception = e
}
return Configuration(
uri,
credentials.userName,
etebaseSession,
null,
null,
exception
)
}
fun findInitialConfiguration(): Configuration {
try {
if (isServerEtebase()) {
Logger.log.fine("Attempting to login to etebase")
return findInitialConfigurationEtebase()
} else {
Logger.log.fine("Attempting to login to EteSync legacy")
return findInitialConfigurationLegacy()
}
} catch (e: Exception) {
return Configuration(
credentials.uri,
credentials.userName,
null,
null,
null,
e
)
}
}
// data classes
@ -76,7 +138,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
class Configuration
// We have to use URI here because HttpUrl is not serializable!
(val url: URI, val userName: String, val authtoken: String?, val cardDAV: ServiceInfo, val calDAV: ServiceInfo, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable {
(val url: URI?, val userName: String, val etebaseSession: String?, val authtoken: String?, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable {
var rawPassword: String? = null
var password: String? = null
var keyPair: Crypto.AsymmetricKeyPair? = null
@ -84,6 +146,9 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
val isFailed: Boolean
get() = this.error != null
val isLegacy: Boolean
get() = this.authtoken != null
class ServiceInfo : Serializable {
val collections: Map<String, CollectionInfo> = HashMap()
@ -93,7 +158,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
}
override fun toString(): String {
return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", cardDAV=" + this.cardDAV + ", calDAV=" + this.calDAV + ", error=" + this.error + ", failed=" + this.isFailed + ")"
return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", error=" + this.error + ", failed=" + this.isFailed + ")"
}
}

@ -0,0 +1,103 @@
/*
* Copyright © 2013 2016 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 com.etesync.syncadapter.ui.setup
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Activity
import android.app.Dialog
import android.app.ProgressDialog
import android.os.Bundle
import android.provider.CalendarContract
import androidx.fragment.app.DialogFragment
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import com.etesync.syncadapter.*
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration
import com.etesync.syncadapter.utils.AndroidCompat
import com.etesync.syncadapter.utils.TaskProviderHandling
import java.util.logging.Level
class CreateAccountFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
return progress
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val config = requireArguments().getSerializable(KEY_CONFIG) as Configuration
val activity = requireActivity()
if (createAccount(config.userName, config)) {
activity.setResult(Activity.RESULT_OK)
activity.finish()
}
}
@Throws(InvalidAccountException::class)
protected fun createAccount(accountName: String, config: Configuration): Boolean {
val account = Account(accountName, App.accountType)
// create Android account
Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url))
val accountManager = AccountManager.get(context)
if (!accountManager.addAccountExplicitly(account, config.password, null))
return false
AccountSettings.setUserData(accountManager, account, config.url, config.userName)
// add entries for account to service DB
Logger.log.log(Level.INFO, "Writing account configuration to database", config)
try {
val settings = AccountSettings(requireContext(), account)
settings.etebaseSession = config.etebaseSession
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong())
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong())
OPENTASK_PROVIDERS.forEach {
// enable task sync if OpenTasks is installed
// further changes will be handled by PackageChangedReceiver
TaskProviderHandling.updateTaskSync(requireContext(), it)
}
} catch (e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Couldn't access account settings", e)
AndroidCompat.removeAccount(accountManager, account)
throw e
}
return true
}
companion object {
private val KEY_CONFIG = "config"
fun newInstance(config: Configuration): CreateAccountFragment {
val frag = CreateAccountFragment()
val args = Bundle(1)
args.putSerializable(KEY_CONFIG, config)
frag.arguments = args
return frag
}
}
}

@ -13,6 +13,7 @@ import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.loader.app.LoaderManager
@ -27,8 +28,8 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.login_configuration_detection)
progress.setMessage(getString(R.string.login_querying_server))
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
@ -38,26 +39,36 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Logger.log.fine("DetectConfigurationFragment: loading")
loaderManager.initLoader(0, arguments, this)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Configuration> {
return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
return ServerConfigurationLoader(requireContext(), args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
}
override fun onLoadFinished(loader: Loader<Configuration>, data: Configuration?) {
if (data != null) {
if (data.isFailed)
// no service found: show error message
fragmentManager!!.beginTransaction()
if (data.isFailed) {
Logger.log.warning("Failed login configuration ${data.error?.localizedMessage}")
// no service found: show error message
requireFragmentManager().beginTransaction()
.add(NothingDetectedFragment.newInstance(data.error!!.localizedMessage), null)
.commitAllowingStateLoss()
else
// service found: continue
fragmentManager!!.beginTransaction()
} else if (data.isLegacy) {
// legacy service found: continue
Logger.log.info("Found legacy account - asking for encryption details")
requireFragmentManager().beginTransaction()
.replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data))
.addToBackStack(null)
.commitAllowingStateLoss()
} else {
Logger.log.info("Found Etebase account account")
requireFragmentManager().beginTransaction()
.replace(android.R.id.content, CreateAccountFragment.newInstance(data))
.addToBackStack(null)
.commitAllowingStateLoss()
}
} else
Logger.log.severe("Configuration detection failed")
@ -71,14 +82,9 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(activity!!)
.setTitle(R.string.login_configuration_detection)
.setTitle(R.string.setting_up_encryption)
.setIcon(R.drawable.ic_error_dark)
.setMessage(R.string.login_wrong_username_or_password)
.setNeutralButton(R.string.login_view_logs) { dialog, which ->
val intent = DebugInfoActivity.newIntent(context, this::class.toString())
intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS))
startActivity(intent)
}
.setMessage(requireArguments().getString(KEY_LOGS))
.setPositiveButton(android.R.string.ok) { dialog, which ->
// dismiss
}

@ -30,7 +30,7 @@ class LoginActivity : BaseActivity() {
if (savedInstanceState == null)
// first call, add fragment
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, LoginCredentialsFragment())
.replace(android.R.id.content, LoginCredentialsFragment(null, null))
.commit()
}
@ -43,16 +43,4 @@ class LoginActivity : BaseActivity() {
fun showHelp(item: MenuItem) {
WebViewActivity.openUrl(this, Constants.helpUri)
}
companion object {
/**
* When set, and [.EXTRA_PASSWORD] is set too, the user name field will be set to this value.
*/
val EXTRA_USERNAME = "username"
/**
* When set, the password field will be set to this value.
*/
val EXTRA_PASSWORD = "password"
}
}

@ -15,22 +15,7 @@ import com.etesync.syncadapter.log.Logger
import java.net.URI
import java.net.URISyntaxException
class LoginCredentials(_uri: URI?, val userName: String, val password: String) : Parcelable {
val uri: URI?
init {
var uri = _uri
if (uri == null) {
try {
uri = URI(Constants.serviceUrl.toString())
} catch (e: URISyntaxException) {
Logger.log.severe("Should never happen, it's a constant")
}
}
this.uri = uri
}
class LoginCredentials(val uri: URI?, val userName: String, val password: String) : Parcelable {
override fun describeContents(): Int {
return 0

@ -12,7 +12,6 @@ import android.accounts.Account
import android.app.Dialog
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
@ -32,8 +31,8 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.login_configuration_detection)
progress.setMessage(getString(R.string.login_querying_server))
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
@ -84,7 +83,7 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(activity!!)
.setTitle(R.string.login_configuration_detection)
.setTitle(R.string.setting_up_encryption)
.setIcon(R.drawable.ic_error_dark)
.setMessage(R.string.login_wrong_username_or_password)
.setNeutralButton(R.string.login_view_logs) { dialog, which ->

@ -17,15 +17,19 @@ import android.widget.CheckedTextView
import android.widget.EditText
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.WebViewActivity
import com.etesync.syncadapter.ui.etebase.SignupFragment
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import net.cachapa.expandablelayout.ExpandableLayout
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URI
class LoginCredentialsFragment : Fragment() {
class LoginCredentialsFragment(private val initialUsername: String?, private val initialPassword: String?) : Fragment() {
internal lateinit var editUserName: EditText
internal lateinit var editUrlPassword: TextInputLayout
@ -36,28 +40,21 @@ class LoginCredentialsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
editUserName = v.findViewById<View>(R.id.user_name) as EditText
editUrlPassword = v.findViewById<View>(R.id.url_password) as TextInputLayout
showAdvanced = v.findViewById<View>(R.id.show_advanced) as CheckedTextView
customServer = v.findViewById<View>(R.id.custom_server) as EditText
editUserName = v.findViewById<TextInputEditText>(R.id.user_name)
editUrlPassword = v.findViewById<TextInputLayout>(R.id.url_password)
showAdvanced = v.findViewById<CheckedTextView>(R.id.show_advanced)
customServer = v.findViewById<TextInputEditText>(R.id.custom_server)
if (savedInstanceState == null) {
val activity = activity
val intent = activity?.intent
if (intent != null) {
// we've got initial login data
val username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME)
val password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD)
editUserName.setText(username)
editUrlPassword.editText?.setText(password)
}
editUserName.setText(initialUsername ?: "")
editUrlPassword.editText?.setText(initialPassword ?: "")
}
val createAccount = v.findViewById<View>(R.id.create_account) as Button
createAccount.setOnClickListener {
val createUri = Constants.registrationUrl.buildUpon().appendQueryParameter("email", editUserName.text.toString()).build()
WebViewActivity.openUrl(context!!, createUri)
parentFragmentManager.commit {
replace(android.R.id.content, SignupFragment(editUserName.text.toString(), editUrlPassword.editText?.text.toString()))
}
}
val login = v.findViewById<View>(R.id.login) as Button
@ -92,12 +89,16 @@ class LoginCredentialsFragment : Fragment() {
if (userName.isEmpty()) {
editUserName.error = getString(R.string.login_email_address_error)
valid = false
} else {
editUserName.error = null
}
val password = editUrlPassword.editText?.text.toString()
if (password.isEmpty()) {
editUrlPassword.error = getString(R.string.login_password_required)
valid = false
} else {
editUrlPassword.error = null
}
var uri: URI? = null
@ -108,6 +109,7 @@ class LoginCredentialsFragment : Fragment() {
val url = server.toHttpUrlOrNull()
if (url != null) {
uri = url.toUri()
customServer.error = null
} else {
customServer.error = getString(R.string.login_custom_server_error)
valid = false

@ -36,8 +36,8 @@ class SetupEncryptionFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.login_encryption_setup_title)
progress.setMessage(getString(R.string.login_encryption_setup))
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
@ -134,13 +134,13 @@ class SetupEncryptionFragment : DialogFragment() {
}
// insert CardDAV service
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV)
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK)
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong())
// insert CalDAV service
insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV)
insertService(accountName, CollectionInfo.Type.CALENDAR)
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong())
@ -160,8 +160,9 @@ class SetupEncryptionFragment : DialogFragment() {
return true
}
protected fun insertService(accountName: String, serviceType: CollectionInfo.Type, info: BaseConfigurationFinder.Configuration.ServiceInfo) {
val data = (context!!.applicationContext as App).data
protected fun insertService(accountName: String, serviceType: CollectionInfo.Type) {
val info = Configuration.ServiceInfo()
val data = (requireContext().applicationContext as App).data
// insert service
val serviceEntity = ServiceEntity.fetchOrCreate(data, accountName, serviceType)

@ -25,8 +25,8 @@ class SetupUserInfoFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.login_encryption_setup_title)
progress.setMessage(getString(R.string.login_encryption_setup))
progress.setTitle(R.string.setting_up_encryption)
progress.setMessage(getString(R.string.setting_up_encryption_content))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false

@ -28,6 +28,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/please_wait"
android:textIsSelectable="true"
android:typeface="monospace" />
</LinearLayout>

@ -40,6 +40,7 @@
android:id="@+id/encryption_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/collection_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/activity_margin">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right" />
</LinearLayout>
<LinearLayout
android:id="@+id/add_member"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:text="@string/collection_members_add"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:src="@drawable/ic_account_add_dark" />
</LinearLayout>
<fragment android:name="com.etesync.syncadapter.ui.etebase.CollectionMembersListFragment"
android:id="@+id/list_entries_container"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp" />
</LinearLayout>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/collection_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/activity_margin">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="@string/collection_members_no_access" />
<Button
android:id="@+id/leave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="14dp"
android:text="@string/collection_members_leave" />
</LinearLayout>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/invitations_accept_reject_dialog"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="center"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="aaaa 1111 bbbb cccc dddd eeee\n1111" />
</LinearLayout>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@id/android:empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/invitations_loading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Title" />
<ImageView
android:id="@+id/read_only"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
android:src="@drawable/ic_readonly_dark"
android:visibility="gone" />
</LinearLayout>

@ -29,19 +29,19 @@
android:text="@string/login_enter_service_details"
android:layout_marginBottom="14dp"/>
<TextView
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_service_details_description"
android:layout_marginBottom="14dp"/>
android:layout_marginBottom="14dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_username"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/url_password"
android:layout_width="match_parent"
@ -81,12 +81,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/custom_server"
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_custom_server"
android:inputType="textUri"/>
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_custom_server"
android:inputType="textUri"/>
</com.google.android.material.textfield.TextInputLayout>
</net.cachapa.expandablelayout.ExpandableLayout>

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="@dimen/activity_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/login_type_headline"
android:text="@string/signup_title"
android:layout_marginBottom="14dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_username"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/url_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:autofillHints="password"
android:inputType="textPassword"
android:hint="@string/login_password"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_encryption_check_password"/>
<CheckedTextView
android:id="@+id/show_advanced"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginLeft="4dp"
android:textSize="18sp"
android:gravity="center_vertical"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:text="@string/login_toggle_advanced" />
<net.cachapa.expandablelayout.ExpandableLayout
android:id="@+id/advanced_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_custom_server"
android:inputType="textUri"/>
</com.google.android.material.textfield.TextInputLayout>
</net.cachapa.expandablelayout.ExpandableLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/stepper_nav_bar">
<Button
android:id="@+id/login"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/login_login"
style="@style/stepper_nav_button"/>
<Space
android:layout_width="0dp"
android:layout_weight="1"
style="@style/stepper_nav_button"/>
<Button
android:id="@+id/create_account"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/login_signup"
style="@style/stepper_nav_button"/>
</LinearLayout>
</LinearLayout>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/collection_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/activity_margin"
android:text="@string/change_journal_title"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/tasks_not_showing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/activity_margin"
android:visibility="gone"
android:text="@string/tasks_not_showing" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/activity_margin">
<TextView
android:id="@+id/stats"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Stats:" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right" />
</LinearLayout>
<fragment android:name="com.etesync.syncadapter.ui.etebase.ListEntriesFragment"
android:id="@+id/list_entries_container"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_add_light" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -25,6 +25,10 @@
android:title="@string/account_show_fingerprint"
app:showAsAction="ifRoom"/>
<item android:id="@+id/invitations"
android:title="@string/invitations_title"
app:showAsAction="never"/>
<item android:id="@+id/delete_account"
android:title="@string/account_delete"
app:showAsAction="never"/>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/journal_item_menu_event_invite">
<item android:title="@string/calendar_attendees_send_email_action"
android:id="@+id/on_send_event_invite"
android:icon="@drawable/ic_email_black"
app:showAsAction="always" />
</group>
<group android:id="@+id/journal_item_menu_restore">
<item android:title="@string/journal_item_restore_action"
android:icon="@drawable/ic_restore_black"
android:id="@+id/on_restore_item"
app:showAsAction="never" />
</group>
</menu>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © 2013 2016 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/on_delete"
android:icon="@drawable/ic_delete_dark"
android:title="@string/delete_collection"
app:showAsAction="always" />
<item
android:id="@+id/on_save"
android:icon="@drawable/ic_save_dark"
android:title="@string/create_collection_create"
app:showAsAction="always" />
</menu>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/view_collection_edit"
android:id="@+id/on_edit"
android:icon="@drawable/ic_edit_dark"
app:showAsAction="always" />
<item android:title="@string/view_collection_members"
android:id="@+id/on_manage_members"
android:icon="@drawable/ic_members_dark"
app:showAsAction="ifRoom"/>
<item android:title="@string/view_collection_import"
android:id="@+id/on_import"
app:showAsAction="never"/>
</menu>

@ -1,211 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- common strings -->
<string name="help">Hilfe</string>
<string name="manage_accounts">Konten verwalten</string>
<string name="please_wait">Bitte warten …</string>
<string name="send">Senden</string>
<!-- startup dialogs -->
<string name="startup_battery_optimization">Akku-Leistungsoptimierung</string>
<string name="startup_battery_optimization_message">Android kann EteSync Synchronisation nach ein paar Tagen abschalten/reduzieren. Um dies zu verhindern, schalten Sie die Akku-Leistungsoptimierung ab.</string>
<string name="startup_battery_optimization_disable">Für EteSync abschalten</string>
<string name="startup_dont_show_again">Nicht erneut zeigen</string>
<string name="startup_development_version">EteSync Vorschau-Version</string>
<string name="startup_development_version_message">Dies ist eine Entwicklungsversion von EteSync. Bitte beachten Sie, dass manche Funktionen nicht wie erwartet funktionieren können. Bitte geben Sie uns konstruktives Feedback um EteSync zu verbessern.</string>
<string name="startup_development_version_give_feedback">Feedback geben</string>
<!-- AboutActivity -->
<string name="about_license_terms">Lizenzbedingungen</string>
<string name="about_license_info_no_warranty">Dieses Programm wird OHNE JEDE GEWÄHRLEISTUNG bereitgestellt. Es ist freie Software, Sie können es also unter bestimmten Bedingungen weiterverbreiten.</string>
<!-- global settings -->
<string name="logging_davdroid_file_logging">EteSync file logging</string>
<string name="logging_to_external_storage">Logging auf externenen Speicher: %s</string>
<string name="logging_to_external_storage_warning">Logs so schnell wie möglich löschen!</string>
<string name="logging_couldnt_create_file">Externe Logdatei konnte nicht erstellt werden: %s</string>
<string name="logging_no_external_storage">Externer Speicher nicht gefunden</string>
<!-- AccountsActivity -->
<string name="navigation_drawer_open">Hauptmenü anzeigen</string>
<string name="navigation_drawer_close">Hauptmenü schließen</string>
<string name="navigation_drawer_subtitle">Sicherer Sync-Adapter</string>
<string name="navigation_drawer_about">Über / Lizenz</string>
<string name="navigation_drawer_settings">Einstellungen</string>
<string name="navigation_drawer_external_links">Externe Links</string>
<string name="navigation_drawer_website">Website</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_guide">Benutzerhandbuch</string>
<string name="navigation_drawer_report_issue">Fehler melden</string>
<string name="navigation_drawer_contact">Entwickler kontaktieren</string>
<string name="account_list_empty">Willkommen bei EteSync!\n\nFügen Sie ein Konto hinzu, um zu beginnen.</string>
<!-- AccountUpdateService -->
<!-- AppSettingsActivity -->
<string name="app_settings">Einstellungen</string>
<string name="app_settings_user_interface">Nutzeroberfläche</string>
<string name="app_settings_reset_hints">Tipps zurücksetzen</string>
<string name="app_settings_reset_hints_summary">Reaktiviert Tipps, die zuvor deaktiviert wurden</string>
<string name="app_settings_reset_hints_success">Alle Tipps werden wieder angezeigt</string>
<string name="app_settings_connection">Verbindung</string>
<string name="app_settings_override_proxy">Proxy-Einstellungen überschreiben</string>
<string name="app_settings_override_proxy_on">Eigene Proxy-Einstellungen werden verwendet</string>
<string name="app_settings_override_proxy_off">Proxy-Einstellungen des Systems werden verwendet</string>
<string name="app_settings_override_proxy_host">HTTP Proxy Host-Name</string>
<string name="app_settings_override_proxy_port">HTTP Proxy Port</string>
<string name="app_settings_security">Sicherheit</string>
<string name="app_settings_distrust_system_certs">Systemzertifikaten misstrauen</string>
<string name="app_settings_distrust_system_certs_on">System- und installierten Zertifikat-Autoritäten wird nicht vertraut</string>
<string name="app_settings_distrust_system_certs_off">System- und installierten Zertifikat-Autoritäten wird vertraut (empfohlen)</string>
<string name="app_settings_reset_certificates">Vertrauen in Zertifikate zurücksetzen</string>
<string name="app_settings_reset_certificates_summary">Setzt Vertrauen/Misstrauen in alle eigenen Zertifikate zurück</string>
<string name="app_settings_reset_certificates_success">Alle eigenen Zertifikate wurden zurückgesetzt</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_log_to_external_storage">Log auf externen Speicher schreiben</string>
<string name="app_settings_log_to_external_storage_on">Logging auf externen Speicher schreiben (wenn verfügbar)</string>
<string name="app_settings_log_to_external_storage_off">Logging auf externen Speicher ist deaktiviert</string>
<string name="app_settings_show_debug_info">Debug-Informationen anzeigen</string>
<string name="app_settings_show_debug_info_details">Software- und Konfigurationsdetails anzeigen/teilen</string>
<!-- AccountActivity -->
<string name="account_synchronize_now">Jetzt synchronisieren</string>
<string name="account_synchronizing_now">Synchronisation gestartet</string>
<string name="account_settings">Konto-Einstellungen</string>
<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 lokalen gespeicherten Kopien von Addressbüchern, Kalendern und Aufgabenlisten werden gelöscht.</string>
<string name="account_delete_collection_last_title">Letzte Sammlung kann nicht gelöscht werden</string>
<string name="account_delete_collection_last_text">Die letzte Sammlung kann nicht gelöscht werden, bitte erstellen Sie eine neue Sammlung wenn Sie diese löschen wollen.</string>
<!-- PermissionsActivity -->
<string name="permissions_title">EteSync-Berechtigungen</string>
<string name="permissions_calendar">Kalender-Zugriff</string>
<string name="permissions_calendar_details">Damit EteSync Kalender-Ereignisse mit Ihren lokalen Kalendern synchronisieren kann, muss EteSync auf Ihre Kalender zugreifen.</string>
<string name="permissions_calendar_request">Kalender-Zugriff anfordern</string>
<string name="permissions_contacts">Kontakt-Zugriff</string>
<string name="permissions_contacts_details">Damit EteSync Addressbüchern mit Ihren lokalen Kontakten synchronisieren kann, muss EteSync auf Ihre Kontakte zugreifen.</string>
<string name="permissions_contacts_request">Kontakt-Zugriff anfordern</string>
<string name="permissions_opentasks">OpenTasks-Zugriff</string>
<string name="permissions_opentasks_details">Damit EteSync Aufgaben mit Ihren lokalen Aufgaben-Listen synchronisieren kann, muss EteSync auf OpenTasks zugreifen.</string>
<string name="permissions_opentasks_request">OpenTasks-Zugriff anfordern</string>
<!-- AddAccountActivity -->
<string name="login_title">Konto hinzufügen</string>
<string name="login_email_address">Email-Adresse</string>
<string name="login_email_address_error">Gültige Email-Adresse benötigt</string>
<string name="login_password">Passwort</string>
<string name="login_encryption_password">Verschlüsselungs-Passwort</string>
<string name="login_encryption_check_password">* Bitte überprüfen Sie das Passwort mehrfach, da es nicht geändert werden kann, sollte es falsch sein.</string>
<string name="login_password_required">Passwort benötigt</string>
<string name="login_login">Anmeldenm</string>
<string name="login_signup">Registrieren</string>
<string name="login_finish">Fertigstellen</string>
<string name="login_back">Zurück</string>
<string name="login_enter_service_details">Anmelde-Daten eingeben</string>
<string name="login_enter_encryption_details">Geheimes Verschlüsselungs-Passwort</string>
<string name="login_encryption_account_label">Konto:</string>
<string name="login_service_details_description">Dies ist Ihr Anmelde-Passwort, *nicht* Ihr Verschlüsselungs-Passwort!</string>
<string name="login_configuration_detection">Ressourcen-Erkennung</string>
<string name="login_querying_server">Server-Informationen werden abgerufen. Bitte warten…</string>
<string name="login_wrong_username_or_password">Authentifizierung fehlgeschlagen (vermutlich aufgrund eines falschen Nutzernamens oder Passworts).</string>
<string name="login_view_logs">Logs anzeigen</string>
<string name="login_encryption_setup_title">Verschlüsselung einstellen</string>
<string name="login_encryption_setup">Bitte warten, Verschlüsselung wird eingestellt…</string>
<!-- AccountSettingsActivity -->
<string name="settings_title">Einstellungen: %s</string>
<string name="settings_authentication">Anmeldeinformationen</string>
<string name="settings_encryption">Verschlüsselung</string>
<string name="settings_password">Passwort</string>
<string name="settings_password_summary">Ihr Anmelde-Passwort</string>
<string name="settings_enter_password">Passwort eingeben:</string>
<string name="settings_encryption_password">Verschlüsselungs-Passwort</string>
<string name="settings_encryption_password_summary">Verschlüsselungs-Passwort ändern</string>
<string name="settings_enter_encryption_password">Verschlüsselungs-Passwort eingeben:</string>
<string name="settings_sync">Synchronisation</string>
<string name="settings_sync_interval_contacts">Häufigkeit der Kontakte-Synchronisation</string>
<string name="settings_sync_summary_manually">Nur manuell</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s und sofort bei lokalen Änderungen</string>
<string name="settings_sync_summary_not_available">Nicht verfügbar</string>
<string name="settings_sync_interval_calendars">Häufigkeit der Kalender-Synchronisation</string>
<string-array name="settings_sync_interval_names">
<item>Nur manuell</item>
<item>Alle 15 Minuten</item>
<item>Jede Stunde</item>
<item>Alle 2 Stunde</item>
<item>Alle 4 Stunde</item>
<item>Einmal täglich</item>
</string-array>
<string name="settings_sync_wifi_only">Nur über WLAN synchronisieren</string>
<string name="settings_sync_wifi_only_on">Synchronisation nur bei aktiver WLAN-Verbindung</string>
<string name="settings_sync_wifi_only_off">Verbindungstyp wird nicht beachtet</string>
<string name="settings_sync_wifi_only_ssid">WLAN-SSID-Einschränkung</string>
<string name="settings_sync_wifi_only_ssid_on">Synchronisation nur über %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle WLAN-Verbindungen können verwendet werden</string>
<string name="settings_sync_wifi_only_ssid_message">Name eines WLANs (SSID) eingeben, um Synchronisierung auf dieses WLAN zu beschränken. Leer lassen, um alle WLAN-Verbindungen zuzulassen.</string>
<string name="settings_carddav">Kontakte</string>
<string name="settings_caldav">Kalender</string>
<!-- collection management -->
<string name="create_addressbook">Adressbuch erstellen</string>
<string name="create_addressbook_display_name_hint">Meine Kontakte</string>
<string name="create_calendar">Kalender erstellen</string>
<string name="create_calendar_display_name_hint">Mein Kalender</string>
<string name="create_collection_color">Farbe des Kalenders einstellen</string>
<string name="create_collection_creating">Sammlung erstellen</string>
<string name="create_collection_display_name">Anzeigename (Titel) dieser Sammlung:</string>
<string name="create_collection_display_name_required">Titel benötigt</string>
<string name="create_collection_description">Beschreibung (optional):</string>
<string name="create_collection_create">Erstellen</string>
<string name="delete_collection">Löschen</string>
<string name="delete_collection_confirm_title">Sind Sie sicher?</string>
<string name="delete_collection_confirm_warning">Diese Sammlung (%s) wird samt allen Inhalten vom Server gelöscht.</string>
<string name="delete_collection_deleting_collection">Sammlung löschen</string>
<!-- ExceptionInfoFragment -->
<string name="exception">Ein Fehler ist aufgetreten.</string>
<string name="exception_httpexception">Ein HTTP-Fehler ist aufgetreten.</string>
<string name="exception_ioexception">Ein E/A-Fehler ist aufgetreten.</string>
<string name="exception_show_details">Details anzeigen</string>
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Informationen zur Fehlersuche</string>
<string name="sync_error_permissions">EteSync-Berechtigungen</string>
<string name="sync_error_permissions_text">Zusätzliche Berechtigungen benötigt</string>
<string name="sync_error_calendar">Kalender-Synchronisation fehlgeschlagen (%s)</string>
<string name="sync_error_contacts">Kontakte-Synchronisation fehlgeschlagen (%s)</string>
<string name="sync_error">Fehler beim %s</string>
<string name="sync_error_integrity">Integritätsfehler beim %s</string>
<string name="sync_error_http_dav">Serverfehler beim %s</string>
<string name="sync_error_unavailable">Verbindungsfehler beim %s</string>
<string name="sync_error_local_storage">Datenbankfehler beim %s</string>
<string name="sync_phase_prepare">Synchronisation wird vorbereitet</string>
<string name="sync_phase_journals">Synchronisiere Journale</string>
<string name="sync_phase_prepare_fetch">Frage Ressourcen ab</string>
<string name="sync_phase_prepare_local">Bereite lokale Einträge vorbereitet</string>
<string name="sync_phase_create_local_entries">Erstelle lokale Einträge</string>
<string name="sync_phase_fetch_entries">Rufe ferne Einträge ab</string>
<string name="sync_phase_apply_remote_entries">Wende entfernte Einträge an</string>
<string name="sync_phase_apply_local_entries">Wende entfernte Einträge erneut an</string>
<string name="sync_phase_push_entries">Lade Einträge hoch</string>
<string name="sync_phase_post_processing">Nachbearbeitung</string>
<string name="sync_error_unauthorized">Authentifizierung fehlgeschlagen</string>
<string name="sync_error_user_inactive">Nutzer ist inaktiv</string>
<!-- Calendar invites -->
<string name="sync_calendar_attendees_notification_content">Einladungen an Gäste versenden?</string>
<string name="calendar_attendees_send_email_action">Versende Einladungen</string>
<string name="sync_calendar_attendees_email_subject" formatted="false">Einladung: %s @ %s</string>
<string name="sync_calendar_attendees_email_content" formatted="false">Sie wurden zu folgender Veranstaltung eingeladen:\n\n%s\nWann: %s\nWo: %s\nTeilnehmer: %s\n\n--\nGesendet mit EteSync - sichere, Ende-zu-Ende verschlüsselte und private Datensynchronisation.\nErhältlich unter https://www.EteSync.com</string>
<!-- cert4android -->
<string name="certificate_notification_connection_security">EteSync: Verbindungs-Sicherheit</string>
<string name="trust_certificate_unknown_certificate_found">EteSync hat ein unbekanntes Zertifikat vorgefunden. Wollen Sie ihm vertrauen?</string>
<!-- common strings -->
<string name="help">Hilfe</string>
<string name="manage_accounts">Konten verwalten</string>
<string name="please_wait">Bitte warten …</string>
<string name="send">Senden</string>
<!-- startup dialogs -->
<string name="startup_battery_optimization">Akku-Leistungsoptimierung</string>
<string name="startup_battery_optimization_message">Android kann EteSync Synchronisation nach ein paar Tagen abschalten/reduzieren. Um dies zu verhindern, schalten Sie die Akku-Leistungsoptimierung ab.</string>
<string name="startup_battery_optimization_disable">Für EteSync abschalten</string>
<string name="startup_dont_show_again">Nicht erneut zeigen</string>
<string name="startup_development_version">EteSync Vorschau-Version</string>
<string name="startup_development_version_message">Dies ist eine Entwicklungsversion von EteSync. Bitte beachten Sie, dass manche Funktionen nicht wie erwartet funktionieren können. Bitte geben Sie uns konstruktives Feedback um EteSync zu verbessern.</string>
<string name="startup_development_version_give_feedback">Rückmeldung geben</string>
<!-- AboutActivity -->
<string name="about_license_terms">Lizenzbedingungen</string>
<string name="about_license_info_no_warranty">Dieses Programm wird OHNE JEDE GEWÄHRLEISTUNG bereitgestellt. Es ist freie Software, Sie können es also unter bestimmten Bedingungen weiterverbreiten.</string>
<!-- global settings -->
<string name="logging_davdroid_file_logging">EteSync-Dateiprotokollierung ist aktiviert</string>
<string name="logging_to_external_storage">Protokollierung auf externenen Speicher: %s</string>
<string name="logging_to_external_storage_warning">Protokolle so schnell wie möglich löschen</string>
<string name="logging_couldnt_create_file">Externe Logdatei konnte nicht erstellt werden: %s</string>
<string name="logging_no_external_storage">Externer Speicher nicht gefunden</string>
<!-- AccountsActivity -->
<string name="navigation_drawer_open">Hauptmenü anzeigen</string>
<string name="navigation_drawer_close">Hauptmenü schließen</string>
<string name="navigation_drawer_subtitle">Sicherer Sync-Adapter</string>
<string name="navigation_drawer_about">Über / Lizenz</string>
<string name="navigation_drawer_settings">Einstellungen</string>
<string name="navigation_drawer_external_links">Externe Links</string>
<string name="navigation_drawer_website">Website</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_guide">Benutzerhandbuch</string>
<string name="navigation_drawer_report_issue">Fehler melden</string>
<string name="navigation_drawer_contact">Entwickler kontaktieren</string>
<string name="account_list_empty">Willkommen bei EteSync!\n\nFügen Sie ein Konto hinzu, um zu beginnen.</string>
<!-- AccountUpdateService -->
<!-- AppSettingsActivity -->
<string name="app_settings">Einstellungen</string>
<string name="app_settings_user_interface">Nutzeroberfläche</string>
<string name="app_settings_reset_hints">Tipps zurücksetzen</string>
<string name="app_settings_reset_hints_summary">Reaktiviert Tipps, die zuvor deaktiviert wurden</string>
<string name="app_settings_reset_hints_success">Alle Tipps werden wieder angezeigt</string>
<string name="app_settings_connection">Verbindung</string>
<string name="app_settings_override_proxy">Proxy-Einstellungen überschreiben</string>
<string name="app_settings_override_proxy_on">Eigene Proxy-Einstellungen werden verwendet</string>
<string name="app_settings_override_proxy_off">Proxy-Einstellungen des Systems werden verwendet</string>
<string name="app_settings_override_proxy_host">HTTP Proxy Host-Name</string>
<string name="app_settings_override_proxy_port">HTTP Proxy Port</string>
<string name="app_settings_security">Sicherheit</string>
<string name="app_settings_distrust_system_certs">Systemzertifikaten misstrauen</string>
<string name="app_settings_distrust_system_certs_on">System- und installierten Zertifikat-Autoritäten wird nicht vertraut</string>
<string name="app_settings_distrust_system_certs_off">System- und installierten Zertifikat-Autoritäten wird vertraut (empfohlen)</string>
<string name="app_settings_reset_certificates">Vertrauen in Zertifikate zurücksetzen</string>
<string name="app_settings_reset_certificates_summary">Setzt Vertrauen/Misstrauen in alle eigenen Zertifikate zurück</string>
<string name="app_settings_reset_certificates_success">Alle eigenen Zertifikate wurden zurückgesetzt</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_log_to_external_storage">Log auf externen Speicher schreiben</string>
<string name="app_settings_log_to_external_storage_on">Logging auf externen Speicher schreiben (wenn verfügbar)</string>
<string name="app_settings_log_to_external_storage_off">Aktivieren, um Protokollnachrichten in Datei speichern</string>
<string name="app_settings_show_debug_info">Debug-Informationen anzeigen</string>
<string name="app_settings_show_debug_info_details">Software- und Konfigurationsdetails anzeigen/teilen</string>
<!-- AccountActivity -->
<string name="account_synchronize_now">Jetzt synchronisieren</string>
<string name="account_synchronizing_now">Synchronisation gestartet</string>
<string name="account_settings">Konto-Einstellungen</string>
<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 lokalen gespeicherten Kopien von Addressbüchern, Kalendern und Aufgabenlisten werden gelöscht.</string>
<string name="account_delete_collection_last_title">Letzte Sammlung kann nicht gelöscht werden</string>
<string name="account_delete_collection_last_text">Die letzte Sammlung kann nicht gelöscht werden, bitte erstellen Sie eine neue Sammlung wenn Sie diese löschen wollen.</string>
<!-- PermissionsActivity -->
<string name="permissions_title">EteSync-Berechtigungen</string>
<string name="permissions_calendar">Kalender-Zugriff</string>
<string name="permissions_calendar_details">Damit EteSync Kalender-Ereignisse mit Ihren lokalen Kalendern synchronisieren kann, muss EteSync auf Ihre Kalender zugreifen.</string>
<string name="permissions_calendar_request">Kalender-Zugriff anfordern</string>
<string name="permissions_contacts">Kontakt-Zugriff</string>
<string name="permissions_contacts_details">Damit EteSync Addressbüchern mit Ihren lokalen Kontakten synchronisieren kann, muss EteSync auf Ihre Kontakte zugreifen.</string>
<string name="permissions_contacts_request">Kontakt-Zugriff anfordern</string>
<string name="permissions_opentasks">OpenTasks-Zugriff</string>
<string name="permissions_opentasks_details">Damit EteSync Aufgaben mit Ihren lokalen Aufgaben-Listen synchronisieren kann, muss EteSync auf OpenTasks zugreifen.</string>
<string name="permissions_opentasks_request">OpenTasks-Zugriff anfordern</string>
<!-- AddAccountActivity -->
<string name="login_title">Konto hinzufügen</string>
<string name="login_email_address">E-Mail-Adresse</string>
<string name="login_email_address_error">Gültige E-Mail-Adresse benötigt</string>
<string name="login_password">Passwort</string>
<string name="login_encryption_password">Verschlüsselungs-Passwort</string>
<string name="login_encryption_check_password">* Bitte überprüfen Sie das Passwort mehrfach, da es nicht geändert werden kann, sollte es falsch sein.</string>
<string name="login_password_required">Passwort benötigt</string>
<string name="login_login">Anmeldenm</string>
<string name="login_signup">Registrieren</string>
<string name="login_finish">Fertigstellen</string>
<string name="login_back">Zurück</string>
<string name="login_enter_service_details">Anmelde-Daten eingeben</string>
<string name="login_enter_encryption_details">Geheimes Verschlüsselungs-Passwort</string>
<string name="login_encryption_account_label">Konto:</string>
<string name="login_service_details_description">Dies ist Ihr Anmelde-Passwort, *nicht* Ihr Verschlüsselungs-Passwort!</string>
<string name="login_configuration_detection">Ressourcen-Erkennung</string>
<string name="login_querying_server">Serverinformationen werden abgerufen. Bitte warten </string>
<string name="login_wrong_username_or_password">Authentifizierung fehlgeschlagen (vermutlich aufgrund eines falschen Nutzernamens oder Passworts).</string>
<string name="login_view_logs">Logs anzeigen</string>
<string name="setting_up_encryption">Verschlüsselung einstellen</string>
<string name="setting_up_encryption_content">Bitte warten, Verschlüsselung wird eingestellt…</string>
<!-- AccountSettingsActivity -->
<string name="settings_title">Einstellungen: %s</string>
<string name="settings_authentication">Anmeldeinformationen</string>
<string name="settings_encryption">Verschlüsselung</string>
<string name="settings_password">Passwort</string>
<string name="settings_password_summary">Ihr Anmelde-Passwort</string>
<string name="settings_enter_password">Passwort eingeben:</string>
<string name="settings_encryption_password">Verschlüsselungs-Passwort</string>
<string name="settings_encryption_password_summary">Verschlüsselungs-Passwort ändern</string>
<string name="settings_enter_encryption_password">Verschlüsselungs-Passwort eingeben:</string>
<string name="settings_sync">Synchronisation</string>
<string name="settings_sync_interval_contacts">Häufigkeit der Kontakte-Synchronisation</string>
<string name="settings_sync_summary_manually">Nur manuell</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s und sofort bei lokalen Änderungen</string>
<string name="settings_sync_summary_not_available">Nicht verfügbar</string>
<string name="settings_sync_interval_calendars">Häufigkeit der Kalender-Synchronisation</string>
<string-array name="settings_sync_interval_names">
<item>Nur manuell</item>
<item>Alle 15 Minuten</item>
<item>Jede Stunde</item>
<item>Alle 2 Stunde</item>
<item>Alle 4 Stunde</item>
<item>Einmal täglich</item>
</string-array>
<string name="settings_sync_wifi_only">Nur über WLAN synchronisieren</string>
<string name="settings_sync_wifi_only_on">Synchronisation nur bei aktiver WLAN-Verbindung</string>
<string name="settings_sync_wifi_only_off">Verbindungstyp wird nicht beachtet</string>
<string name="settings_sync_wifi_only_ssid">WLAN-SSID-Einschränkung</string>
<string name="settings_sync_wifi_only_ssid_on">Synchronisation nur über %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle WLAN-Verbindungen können verwendet werden</string>
<string name="settings_sync_wifi_only_ssid_message">Name eines WLANs (SSID) eingeben, um Synchronisierung auf dieses WLAN zu beschränken. Leer lassen, um alle WLAN-Verbindungen zuzulassen.</string>
<string name="settings_carddav">Kontakte</string>
<string name="settings_caldav">Kalender</string>
<!-- collection management -->
<string name="create_addressbook">Adressbuch erstellen</string>
<string name="create_addressbook_display_name_hint">Meine Kontakte</string>
<string name="create_calendar">Kalender erstellen</string>
<string name="create_calendar_display_name_hint">Mein Kalender</string>
<string name="create_collection_color">Farbe des Kalenders einstellen</string>
<string name="create_collection_creating">Sammlung erstellen</string>
<string name="create_collection_display_name">Anzeigename (Titel) dieser Sammlung:</string>
<string name="create_collection_display_name_required">Titel benötigt</string>
<string name="create_collection_description">Beschreibung (optional):</string>
<string name="create_collection_create">Erstellen</string>
<string name="delete_collection">Löschen</string>
<string name="delete_collection_confirm_title">Sind Sie sicher?</string>
<string name="delete_collection_confirm_warning">Diese Sammlung (%s) wird samt allen Inhalten vom Server gelöscht.</string>
<string name="delete_collection_deleting_collection">Sammlung löschen</string>
<!-- ExceptionInfoFragment -->
<string name="exception">Ein Fehler ist aufgetreten.</string>
<string name="exception_httpexception">Ein HTTP-Fehler ist aufgetreten.</string>
<string name="exception_ioexception">Ein E/A-Fehler ist aufgetreten.</string>
<string name="exception_show_details">Details anzeigen</string>
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Informationen zur Fehlersuche</string>
<string name="sync_error_permissions">EteSync-Berechtigungen</string>
<string name="sync_error_permissions_text">Zusätzliche Berechtigungen benötigt</string>
<string name="sync_error_calendar">Kalender-Synchronisation fehlgeschlagen (%s)</string>
<string name="sync_error_contacts">Kontakte-Synchronisation fehlgeschlagen (%s)</string>
<string name="sync_error">Fehler beim %s</string>
<string name="sync_error_integrity">Integritätsfehler beim %s</string>
<string name="sync_error_http_dav">Serverfehler beim %s</string>
<string name="sync_error_unavailable">Verbindungsfehler beim %s</string>
<string name="sync_error_local_storage">Datenbankfehler beim %s</string>
<string name="sync_phase_prepare">Synchronisation wird vorbereitet</string>
<string name="sync_phase_journals">Synchronisiere Journale</string>
<string name="sync_phase_prepare_fetch">Frage Ressourcen ab</string>
<string name="sync_phase_prepare_local">Bereite lokale Einträge vorbereitet</string>
<string name="sync_phase_create_local_entries">Erstelle lokale Einträge</string>
<string name="sync_phase_fetch_entries">Rufe ferne Einträge ab</string>
<string name="sync_phase_apply_remote_entries">Wende entfernte Einträge an</string>
<string name="sync_phase_apply_local_entries">Wende entfernte Einträge erneut an</string>
<string name="sync_phase_push_entries">Lade Einträge hoch</string>
<string name="sync_phase_post_processing">Nachbearbeitung</string>
<string name="sync_error_unauthorized">Authentifizierung fehlgeschlagen</string>
<string name="sync_error_user_inactive">Nutzer ist inaktiv</string>
<!-- Calendar invites -->
<string name="sync_calendar_attendees_notification_content">Einladungen an Gäste versenden?</string>
<string name="calendar_attendees_send_email_action">Versende Einladungen</string>
<string name="sync_calendar_attendees_email_subject" formatted="false">Einladung: %s @ %s</string>
<string name="sync_calendar_attendees_email_content" formatted="false">Sie wurden zu folgender Veranstaltung eingeladen:\n\n%s\nWann: %s\nWo: %s\nTeilnehmer: %s\n\n--\nGesendet mit EteSync - sichere, Ende-zu-Ende verschlüsselte und private Datensynchronisation.\nErhältlich unter https://www.EteSync.com</string>
<!-- cert4android -->
<string name="certificate_notification_connection_security">EteSync: Verbindungs-Sicherheit</string>
<string name="trust_certificate_unknown_certificate_found">EteSync hat ein unbekanntes Zertifikat vorgefunden. Wollen Sie ihm vertrauen?</string>
<string name="app_settings_log_verbose">Ausführliche Protokollierung</string>
</resources>

@ -1,127 +1,195 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="help">Aide</string>
<string name="manage_accounts">Gestion des comptes</string>
<string name="please_wait">SVP attendez ...</string>
<string name="send">Envoyer</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimisation de la batterie</string>
<string name="startup_dont_show_again">Ne plus afficher</string>
<!--AboutActivity-->
<string name="about_license_terms">Conditions d\'utilisation</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>
<!--global settings-->
<string name="logging_to_external_storage">Se connecter au stockage externe: %s</string>
<string name="logging_to_external_storage_warning">Supprimer les journaux dès que possible!</string>
<string name="logging_couldnt_create_file">Impossible de créer le fichier journal externe: %s</string>
<string name="logging_no_external_storage">Stockage externe introuvable</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Ouvrir le tiroir de navigation</string>
<string name="navigation_drawer_close">Fermer le tiroir de navigation</string>
<string name="navigation_drawer_about">A propos / Licence</string>
<string name="navigation_drawer_settings">Paramètres</string>
<string name="navigation_drawer_external_links">Liens externes</string>
<string name="navigation_drawer_website">Site Web</string>
<string name="navigation_drawer_faq">FAQ</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">Paramètres</string>
<string name="app_settings_user_interface">Interface utilisateur</string>
<string name="app_settings_reset_hints">Réinitialiser les astuces</string>
<string name="app_settings_reset_hints_summary">Réactiver les astuces qui ont été vu précédemment</string>
<string name="app_settings_reset_hints_success">Toutes les astuces seront affichés à nouveau</string>
<string name="app_settings_connection">Connexion</string>
<string name="app_settings_override_proxy">Ignorer les paramètres proxy</string>
<string name="app_settings_override_proxy_on">Utiliser des paramètres proxy personnalisés</string>
<string name="app_settings_override_proxy_off">Utiliser les paramètres proxy du système</string>
<string name="app_settings_override_proxy_host">Nom de l\'hôte du proxy HTTP</string>
<string name="app_settings_override_proxy_port">Port du proxy HTTP</string>
<string name="app_settings_security">Sécurité</string>
<string name="app_settings_distrust_system_certs">Révoquer les certificats du système</string>
<string name="app_settings_distrust_system_certs_on">Les certificats du système et ceux ajoutés par l\'utilisateur ne seront pas dignes de confiance</string>
<string name="app_settings_distrust_system_certs_off">Les certificats du système et ceux ajoutés par l\'utilisateur seront dignes de confiance (recommandé)</string>
<string name="app_settings_reset_certificates">Réinitialiser les certificats de (non)confiance</string>
<string name="app_settings_reset_certificates_summary">Réinitialiser la confiance de tous les certificats personnalisés</string>
<string name="app_settings_reset_certificates_success">Tous les certificats personnalisés ont été effacés</string>
<string name="app_settings_debug">Débogage</string>
<string name="app_settings_log_to_external_storage">Journaliser dans un fichier externe</string>
<string name="app_settings_log_to_external_storage_on">Journaliser sur le stockage externe (si disponible)</string>
<string name="app_settings_log_to_external_storage_off">Le fichier externe n\'est pas disponible.</string>
<string name="app_settings_show_debug_info">Afficher les infos de débogage</string>
<string name="app_settings_show_debug_info_details">Voir/partager l\'application et les détails de configuration</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchroniser maintenant</string>
<string name="account_synchronizing_now">Synchronisation en cours</string>
<string name="account_settings">Paramètres du compte</string>
<string name="account_delete">Supprimer le compte</string>
<string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte?</string>
<string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">Autorisations calendrier</string>
<string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string>
<string name="permissions_contacts">Autorisations contacts</string>
<string name="permissions_contacts_request">Demande d\'autorisations d\'accéder aux contacts</string>
<string name="permissions_opentasks">Autorisations OpenTasks</string>
<string name="permissions_opentasks_request">Demande d\'autorisations d\'accéder à OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Ajouter un compte</string>
<string name="login_email_address">Adresse mail</string>
<string name="login_email_address_error">Une adresse e-mail valide est requis</string>
<string name="login_password">Mot de passe</string>
<string name="login_password_required">Mot de passe requis</string>
<string name="login_back">Retour</string>
<string name="login_configuration_detection">Détection de la configuration</string>
<string name="login_querying_server">S\'il vous plaît patienter, nous interrogeons le serveur ...</string>
<string name="login_view_logs">Voir infos de débogage</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paramètres: %s</string>
<string name="settings_authentication">Authentification</string>
<string name="settings_password">Mot de passe</string>
<string name="settings_enter_password">Saisissez votre mot de passe :</string>
<string name="settings_sync">Synchronisation</string>
<string name="settings_sync_interval_contacts">Interval de synchronisation des carnets d\'adresses</string>
<string name="settings_sync_summary_manually">Manuellement</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s et immédiatement après un changement local</string>
<string name="settings_sync_summary_not_available">Indisponible</string>
<string name="settings_sync_interval_calendars">Interval de synchronisation des agendas</string>
<string-array name="settings_sync_interval_names">
<item>Manuellement</item>
<item>Toutes les 15 minutes</item>
<item>Toutes les heures</item>
<item>Toutes les 2 heures</item>
<item>Toutes les 4 heures</item>
<item>Une fois par jour</item>
</string-array>
<string name="settings_sync_wifi_only">Synchronisation en Wifi seulement</string>
<string name="settings_sync_wifi_only_on">La synchronisation est limitée aux connexions WiFi</string>
<string name="settings_sync_wifi_only_off">Le type de connexion n\'est pas pris en charge</string>
<string name="settings_sync_wifi_only_ssid">Restriction WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Sera seulement synchroniser avec %s</string>
<string name="settings_sync_wifi_only_ssid_off">Toutes les connexions WiFi peuvent être utilisées</string>
<string name="settings_sync_wifi_only_ssid_message">Entrez le nom d\'un réseau WiFi (SSID) pour restreindre la synchronisation à ce réseau, ou laisser vide pour autoriser toutes les connexions WiFi.</string>
<!--collection management-->
<string name="create_addressbook">Créer un carnet d\'adresses</string>
<string name="create_addressbook_display_name_hint">Mon carnet d\'adresses</string>
<string name="create_collection_creating">Création collection</string>
<string name="create_collection_display_name">Le nom affiché (titre) pour cette collection:</string>
<string name="create_collection_display_name_required">Titre requis</string>
<string name="create_collection_description">Description (facultatif)</string>
<string name="create_collection_create">Créer</string>
<string name="delete_collection">Supprimer la collection</string>
<string name="delete_collection_confirm_title">Êtes-vous sur?</string>
<string name="delete_collection_confirm_warning">Cette collection (%s) et toutes ses données seront supprimées du serveur.</string>
<string name="delete_collection_deleting_collection">Suppression de la collection</string>
<!--ExceptionInfoFragment-->
<string name="exception">Une erreur est survenue.</string>
<string name="exception_httpexception">Une erreur HTTP est survenue.</string>
<string name="exception_ioexception">Une erreur I/O est survenue.</string>
<string name="exception_show_details">Voir détails</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Infos de débogage</string>
<string name="sync_error_calendar">Échec de la synchronisation du calendrier (%s)</string>
<string name="sync_error">Erreur durant %s</string>
<string name="sync_error_http_dav">Erreur de serveur durant %s</string>
<string name="sync_error_local_storage">Erreur de base de donnée durant %s</string>
<!--cert4android-->
</resources>
<!--common strings-->
<string name="help">Aide</string>
<string name="manage_accounts">Gestion des comptes</string>
<string name="please_wait">Veuillez patienter…</string>
<string name="send">Envoyer</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimisation de la batterie</string>
<string name="startup_dont_show_again">Ne plus afficher</string>
<!--AboutActivity-->
<string name="about_license_terms">Conditions d\'utilisation</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>
<!--global settings-->
<string name="logging_to_external_storage">Journalisation sur le stockage externe : %s</string>
<string name="logging_to_external_storage_warning">Supprimer les journaux dès que possible</string>
<string name="logging_couldnt_create_file">Impossible de créer le fichier journal externe : %s</string>
<string name="logging_no_external_storage">Stockage externe introuvable</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Ouvrir le tiroir de navigation</string>
<string name="navigation_drawer_close">Fermer le tiroir de navigation</string>
<string name="navigation_drawer_about">A propos / Licence</string>
<string name="navigation_drawer_settings">Paramètres</string>
<string name="navigation_drawer_external_links">Liens externes</string>
<string name="navigation_drawer_website">Site Web</string>
<string name="navigation_drawer_faq">FAQ</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">Paramètres</string>
<string name="app_settings_user_interface">Interface utilisateur</string>
<string name="app_settings_reset_hints">Réinitialiser les astuces</string>
<string name="app_settings_reset_hints_summary">Réactiver les astuces qui ont été vu précédemment</string>
<string name="app_settings_reset_hints_success">Toutes les astuces seront affichés à nouveau</string>
<string name="app_settings_connection">Connexion</string>
<string name="app_settings_override_proxy">Ignorer les paramètres proxy</string>
<string name="app_settings_override_proxy_on">Utiliser des paramètres proxy personnalisés</string>
<string name="app_settings_override_proxy_off">Utiliser les paramètres proxy du système</string>
<string name="app_settings_override_proxy_host">Nom de l\'hôte du proxy HTTP</string>
<string name="app_settings_override_proxy_port">Port du proxy HTTP</string>
<string name="app_settings_security">Sécurité</string>
<string name="app_settings_distrust_system_certs">Révoquer les certificats du système</string>
<string name="app_settings_distrust_system_certs_on">Les certificats du système et ceux ajoutés par l\'utilisateur ne seront pas dignes de confiance</string>
<string name="app_settings_distrust_system_certs_off">Les certificats du système et ceux ajoutés par l\'utilisateur seront dignes de confiance (recommandé)</string>
<string name="app_settings_reset_certificates">Réinitialiser les certificats de (non)confiance</string>
<string name="app_settings_reset_certificates_summary">Réinitialiser la confiance de tous les certificats personnalisés</string>
<string name="app_settings_reset_certificates_success">Tous les certificats personnalisés ont été effacés</string>
<string name="app_settings_debug">Débogage</string>
<string name="app_settings_log_to_external_storage">Journaliser dans un fichier externe</string>
<string name="app_settings_log_to_external_storage_on">Journaliser sur le stockage externe (si disponible)</string>
<string name="app_settings_log_to_external_storage_off">Activer pour enregistrer les messages de journalisation sur le fichier</string>
<string name="app_settings_show_debug_info">Afficher les infos de débogage</string>
<string name="app_settings_show_debug_info_details">Voir/partager l\'application et les détails de configuration</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchroniser maintenant</string>
<string name="account_synchronizing_now">Synchronisation en cours</string>
<string name="account_settings">Paramètres du compte</string>
<string name="account_delete">Supprimer le compte</string>
<string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte \?</string>
<string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">Autorisations calendrier</string>
<string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string>
<string name="permissions_contacts">Autorisations contacts</string>
<string name="permissions_contacts_request">Demande d\'autorisations d\'accéder aux contacts</string>
<string name="permissions_opentasks">Autorisations OpenTasks</string>
<string name="permissions_opentasks_request">Demande d\'autorisations d\'accéder à OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Ajouter un compte</string>
<string name="login_email_address">Adresse courriel</string>
<string name="login_email_address_error">Une adresse courriel valide est requise</string>
<string name="login_password">Mot de passe</string>
<string name="login_password_required">Mot de passe requis</string>
<string name="login_back">Retour</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_view_logs">Voir infos de débogage</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paramètres : %s</string>
<string name="settings_authentication">Authentification</string>
<string name="settings_password">Mot de passe</string>
<string name="settings_enter_password">Saisissez votre mot de passe :</string>
<string name="settings_sync">Synchronisation</string>
<string name="settings_sync_interval_contacts">Interval de synchronisation des carnets d\'adresses</string>
<string name="settings_sync_summary_manually">Manuellement</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s et immédiatement après un changement local</string>
<string name="settings_sync_summary_not_available">Indisponible</string>
<string name="settings_sync_interval_calendars">Interval de synchronisation des agendas</string>
<string-array name="settings_sync_interval_names">
<item>Manuellement</item>
<item>Toutes les 15 minutes</item>
<item>Toutes les heures</item>
<item>Toutes les 2 heures</item>
<item>Toutes les 4 heures</item>
<item>Une fois par jour</item>
</string-array>
<string name="settings_sync_wifi_only">Synchronisation en Wifi seulement</string>
<string name="settings_sync_wifi_only_on">La synchronisation est limitée aux connexions WiFi</string>
<string name="settings_sync_wifi_only_off">Le type de connexion n\'est pas pris en charge</string>
<string name="settings_sync_wifi_only_ssid">Restriction WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Sera seulement synchroniser avec %s</string>
<string name="settings_sync_wifi_only_ssid_off">Toutes les connexions WiFi peuvent être utilisées</string>
<string name="settings_sync_wifi_only_ssid_message">Entrez le nom d\'un réseau WiFi (SSID) pour restreindre la synchronisation à ce réseau, ou laisser vide pour autoriser toutes les connexions WiFi.</string>
<!--collection management-->
<string name="create_addressbook">Créer un carnet d\'adresses</string>
<string name="create_addressbook_display_name_hint">Mon carnet d\'adresses</string>
<string name="create_collection_creating">Création collection</string>
<string name="create_collection_display_name">Le nom affiché (titre) pour cette collection :</string>
<string name="create_collection_display_name_required">Titre requis</string>
<string name="create_collection_description">Description (facultatif) :</string>
<string name="create_collection_create">Créer</string>
<string name="delete_collection">Supprimer</string>
<string name="delete_collection_confirm_title">Êtes-vous sûr·e \?</string>
<string name="delete_collection_confirm_warning">Cette collection (%s) et toutes ses données seront supprimées du serveur.</string>
<string name="delete_collection_deleting_collection">Suppression de la collection</string>
<!--ExceptionInfoFragment-->
<string name="exception">Une erreur est survenue.</string>
<string name="exception_httpexception">Une erreur HTTP est survenue.</string>
<string name="exception_ioexception">Une erreur I/O est survenue.</string>
<string name="exception_show_details">Voir détails</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Infos de débogage</string>
<string name="sync_error_calendar">Échec de la synchronisation du calendrier (%s)</string>
<string name="sync_error">Erreur durant %s</string>
<string name="sync_error_http_dav">Erreur de serveur durant %s</string>
<string name="sync_error_local_storage">Erreur de base de donnée durant %s</string>
<!--cert4android-->
<string name="trust_fingerprint_title">Vérifier lempreinte de sécurité</string>
<string name="collection_members_adding">Ajout dun membre</string>
<string name="collection_members_add_error">Erreur lors de lajout du membre</string>
<string name="collection_members_add">Ajouter un membre</string>
<string name="collection_members_list_empty">Aucun membre</string>
<string name="collection_members_list_loading">Chargement des membres…</string>
<string name="collection_members_title">Membres</string>
<string name="not_allowed_title">Non autorisé</string>
<string name="show_fingperprint_title">Mon empreinte</string>
<string name="account_click_install_tasks">Cliquez ici pour installer OpenTasks !</string>
<string name="account_delete_collection_last_title">Impossible de supprimer la dernière collection</string>
<string name="account_show_fingerprint">Afficher lempreinte</string>
<string name="app_settings_prefer_tasksorg_snack">Vous devrez peut-être supprimer et rajouter votre compte pour que ces modifications prennent effet.</string>
<string name="app_settings_prefer_tasksorg_off">Se synchronisera avec OpenTasks si disponible</string>
<string name="app_settings_prefer_tasksorg_on">Se synchronisera avec Tasks.org si disponible</string>
<string name="app_settings_prefer_tasksorg">Préférer le fournisseur Tasks.org</string>
<string name="app_settings_sync">Synchroniser</string>
<string name="app_settings_notification_settings_summary">Gérer les canaux de notification et leurs paramètres</string>
<string name="app_settings_notification_settings">Paramètres de notification</string>
<string name="accounts_missing_permissions">Permissions manquantes : %s</string>
<string name="accounts_showcase_add">Vous devez ajouter un compte pour utiliser EteSync. Cliquez ici pour en ajouter un…</string>
<string name="accounts_global_sync_enable">Activer</string>
<string name="account_list_empty">Bienvenue sur EteSync !</string>
<string name="navigation_drawer_contact">Contact</string>
<string name="navigation_drawer_report_issue">Signaler un problème</string>
<string name="navigation_drawer_guide">Guide dutilisation</string>
<string name="navigation_drawer_subtitle">Adaptateur de synchro sécurisé</string>
<string name="startup_vendor_specific_bugs_open_faq">Ouvrir la FAQ</string>
<string name="logging_davdroid_file_logging">La journalisation de fichiers EteSync est activée</string>
<string name="startup_development_version_give_feedback">Envoyer des remarques</string>
<string name="startup_battery_optimization_disable">Éteindre pour EteSync</string>
<string name="tourguide_title">Le saviez-vous \?</string>
<string name="address_books_authority_title">Carnet dadresses</string>
<string name="account_title_address_book">Carnet dadresses EteSync</string>
<string name="view_collection_edit">Modifier</string>
<string name="edit_collection">Modifier la collection</string>
<string name="edit_owner_only">Seul le propriétaire de cette collection (%s) est autorisé à la modifier.</string>
<string name="app_settings_change_notification">Afficher les notifications de changement</string>
<string name="app_settings_force_language_default">Auto</string>
<string name="app_settings_force_language">Forcer la langue</string>
<string name="sync_calendar_attendees_email_content" formatted="false">Vous avez été invité·e à l\'évènement suivant :
\n
\n% s
\nQuand : %s
\nOù : %s
\nParticipants : %s
\n
\n-
\nEnvoyé par EteSync synchronisation sécurisée, chiffrée de bout en bout et privée des données.
\nObtenez-le sur https://www.EteSync.com</string>
<string name="sync_calendar_attendees_email_subject" formatted="false">Invitation : %s @ %s</string>
<string name="calendar_attendees_send_email_action">Envoyer des invitations</string>
<string name="debug_info_more_data_shared">Cliquez sur Partager pour ouvrir l\'application de messagerie avec les données ci-dessous, ainsi que des informations de débogage supplémentaires, jointes. Il peut contenir des informations sensibles, veuillez donc l\'examiner avant de l\'envoyer.</string>
<string name="journal_item_email">Courriel</string>
<string name="crash_email_body">Si possible, veuillez inclure toute autre information pertinente telle que ce que vous avez fait pour déclencher cette erreur.</string>
<string name="crash_email_subject">Informations sur le débogage EteSync</string>
<string name="crash_title">EteSync a planté !</string>
<string name="notification_channel_sync_status_desc">Messages détat informatifs comme le résumé des modifications de synchronisation</string>
<string name="notification_channel_sync_status">Messages détat</string>
<string name="notification_channel_sync_warnings_desc">Problèmes de synchronisation non fatals tels que les modifications ignorées des collections en lecture seule</string>
<string name="notification_channel_sync_warnings">Alertes de synchronisation</string>
<string name="notification_channel_sync_errors_desc">Erreurs importantes qui arrêtent la synchronisation, telles que des réponses inattendues du serveur</string>
<string name="notification_channel_sync_errors">Erreurs de synchronisation</string>
<string name="notification_channel_sync">Synchronisation</string>
<string name="notification_channel_general">Autres messages importants</string>
<string name="notification_channel_debugging">Débogage</string>
<string name="notification_channel_crash_reports">Rapports de plantage</string>
<string name="app_name">EteSync</string>
</resources>

@ -1,119 +1,125 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="help">Aiuto</string>
<string name="manage_accounts">Gestione account</string>
<string name="please_wait">Attendere prego …</string>
<string name="send">Invia</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Ottimizazione della batteria</string>
<string name="startup_dont_show_again">Non mostrare più</string>
<!--AboutActivity-->
<string name="about_license_terms">Termini di licenza</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
<string name="logging_to_external_storage">Log su dispositivo esterno: %s</string>
<string name="logging_to_external_storage_warning">Cancellare prima possibile i file di log!</string>
<string name="logging_couldnt_create_file">Non riesco a creare il file di log esterno: %s</string>
<string name="logging_no_external_storage">Dispositivo esterno non disponibile</string>
<!--AccountsActivity-->
<string name="navigation_drawer_about">Informazioni / Licenza</string>
<string name="navigation_drawer_settings">Impostazioni</string>
<string name="navigation_drawer_external_links">Link esterni</string>
<string name="navigation_drawer_website">Sito web</string>
<string name="navigation_drawer_faq">Domande Frequenti</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">Impostazioni</string>
<string name="app_settings_user_interface">Interfaccia utente</string>
<string name="app_settings_reset_hints">Reimposta i suggerimenti</string>
<string name="app_settings_reset_hints_summary">Riabilita i suggerimenti precedentemente disabilitati</string>
<string name="app_settings_reset_hints_success">I suggerimenti verranno mostrati</string>
<string name="app_settings_connection">Connessione</string>
<string name="app_settings_override_proxy">Non rispettare la impostazioni del proxy</string>
<string name="app_settings_override_proxy_on">Impostazioni personalizzate del proxy</string>
<string name="app_settings_override_proxy_off">Usa le impostazioni di sistema del proxy</string>
<string name="app_settings_override_proxy_host">Nome host del proxy HTTP</string>
<string name="app_settings_override_proxy_port">Porta del proxy HTTP</string>
<string name="app_settings_security">Sicurezza</string>
<string name="app_settings_distrust_system_certs">Non ti fidare dei certificati di sistema</string>
<string name="app_settings_distrust_system_certs_on">Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili</string>
<string name="app_settings_distrust_system_certs_off">Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato)</string>
<string name="app_settings_reset_certificates">Reimposta la fiducia in tutti i certificati</string>
<string name="app_settings_reset_certificates_summary">Reimposta la fiducia nei certificati aggiunti</string>
<string name="app_settings_reset_certificates_success">Sono stati cancellati tutti i certificati aggiunti</string>
<string name="app_settings_debug">Debug</string>
<string name="app_settings_log_to_external_storage">Log su file esterno</string>
<string name="app_settings_log_to_external_storage_on">Log su dispositivo esterno (se disponibile)</string>
<string name="app_settings_log_to_external_storage_off">Log su file esterno disabilitato</string>
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
<string name="app_settings_show_debug_info_details">Mostra e condividi i dettagli del programma e della configurazione</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Sincronizza adesso</string>
<string name="account_synchronizing_now">Sincronizzazione in corso</string>
<string name="account_settings">Impostazioni account</string>
<string name="account_delete">Elimina account</string>
<string name="account_delete_confirmation_title">Cancellare l\'account?</string>
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">Permessi calendario</string>
<string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string>
<string name="permissions_contacts">Permessi Contatti</string>
<string name="permissions_contacts_request">Richiesta autorizzazione ai contatti</string>
<string name="permissions_opentasks">Permessi OpenTasks</string>
<string name="permissions_opentasks_request">Richiesta autorizzazione ad OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Aggiungi account</string>
<string name="login_email_address">Indirizzo email</string>
<string name="login_email_address_error">È necessario un indirizzo email valido</string>
<string name="login_password">Password</string>
<string name="login_password_required">Password richiesta</string>
<string name="login_back">Indietro</string>
<string name="login_configuration_detection">Rilevazione configurazione</string>
<string name="login_querying_server">Attendere, invio richiesta al server...</string>
<string name="login_view_logs">Vedi i log</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Impostazioni: %s</string>
<string name="settings_authentication">Autenticazione</string>
<string name="settings_password">Password</string>
<string name="settings_enter_password">Inserisci la tua password:</string>
<string name="settings_sync">Sincronizzazione</string>
<string name="settings_sync_interval_contacts">Intervallo sincr. Contatti</string>
<string name="settings_sync_summary_manually">Solo manualmente</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s e a seguito di ogni cambiamento locale</string>
<string name="settings_sync_summary_not_available">Non disponile</string>
<string name="settings_sync_interval_calendars">Intervallo sincr. calendari</string>
<string-array name="settings_sync_interval_names">
<item>Solo manualmente</item>
<item>Ogni 15 minuti</item>
<item>Ogni ora</item>
<item>Ogni 2 ore</item>
<item>Ogni 4 ore</item>
<item>Una volta al giorno</item>
</string-array>
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
<string name="settings_sync_wifi_only_ssid">Restrizione sul SSID del WiFi</string>
<string name="settings_sync_wifi_only_ssid_on">La sincronizzazione avverrà solo con %s</string>
<string name="settings_sync_wifi_only_ssid_off">Verranno usate tutte le connessioni WiFi</string>
<string name="settings_sync_wifi_only_ssid_message">Inserisci il nome di una rete WiFi (SSID) per limitare la sincronizzazione solo con questa rete o lasciare in bianco per usare tutte le connessioni WiFi.</string>
<!--collection management-->
<string name="create_addressbook">Crea indirizzario</string>
<string name="create_addressbook_display_name_hint">Il mio indirizzario</string>
<string name="create_collection_display_name_required">Il titolo è richiesto</string>
<string name="create_collection_description">Descrizione (opzionale):</string>
<string name="create_collection_create">Crea</string>
<string name="delete_collection_confirm_title">Sei sicuro?</string>
<!--ExceptionInfoFragment-->
<string name="exception">Si è verificato un errore.</string>
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
<string name="exception_ioexception">Si è verificato un errore di I/O.</string>
<string name="exception_show_details">Mostra dettagli</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Informazioni di debug</string>
<string name="sync_error_calendar">Sincronizzazione del calendario fallita (%s)</string>
<string name="sync_error">Errore nel %s</string>
<string name="sync_error_http_dav">Errore del server nel %s</string>
<string name="sync_error_local_storage">Errore del database nel %s</string>
<!--cert4android-->
</resources>
<!--common strings-->
<string name="help">Aiuto</string>
<string name="manage_accounts">Gestione account</string>
<string name="please_wait">Si prega di attendere…</string>
<string name="send">Invia</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Ottimizazione della batteria</string>
<string name="startup_dont_show_again">Non mostrare più</string>
<!--AboutActivity-->
<string name="about_license_terms">Termini di licenza</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
<string name="logging_to_external_storage">Log su dispositivo esterno: %s</string>
<string name="logging_to_external_storage_warning">Cancellare prima possibile i file di log!</string>
<string name="logging_couldnt_create_file">Non riesco a creare il file di log esterno: %s</string>
<string name="logging_no_external_storage">Dispositivo esterno non disponibile</string>
<!--AccountsActivity-->
<string name="navigation_drawer_about">Informazioni / Licenza</string>
<string name="navigation_drawer_settings">Impostazioni</string>
<string name="navigation_drawer_external_links">Link esterni</string>
<string name="navigation_drawer_website">Sito web</string>
<string name="navigation_drawer_faq">Domande Frequenti</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">Impostazioni</string>
<string name="app_settings_user_interface">Interfaccia utente</string>
<string name="app_settings_reset_hints">Reimposta i suggerimenti</string>
<string name="app_settings_reset_hints_summary">Riabilita i suggerimenti precedentemente disabilitati</string>
<string name="app_settings_reset_hints_success">I suggerimenti verranno mostrati</string>
<string name="app_settings_connection">Connessione</string>
<string name="app_settings_override_proxy">Non rispettare la impostazioni del proxy</string>
<string name="app_settings_override_proxy_on">Impostazioni personalizzate del proxy</string>
<string name="app_settings_override_proxy_off">Usa le impostazioni di sistema del proxy</string>
<string name="app_settings_override_proxy_host">Nome host del proxy HTTP</string>
<string name="app_settings_override_proxy_port">Porta del proxy HTTP</string>
<string name="app_settings_security">Sicurezza</string>
<string name="app_settings_distrust_system_certs">Non ti fidare dei certificati di sistema</string>
<string name="app_settings_distrust_system_certs_on">Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili</string>
<string name="app_settings_distrust_system_certs_off">Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato)</string>
<string name="app_settings_reset_certificates">Reimposta la fiducia in tutti i certificati</string>
<string name="app_settings_reset_certificates_summary">Reimposta la fiducia nei certificati aggiunti</string>
<string name="app_settings_reset_certificates_success">Sono stati cancellati tutti i certificati aggiunti</string>
<string name="app_settings_debug">Debug</string>
<string name="app_settings_log_to_external_storage">Log su file esterno</string>
<string name="app_settings_log_to_external_storage_on">Log su dispositivo esterno (se disponibile)</string>
<string name="app_settings_log_to_external_storage_off">Log su file esterno disabilitato</string>
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
<string name="app_settings_show_debug_info_details">Mostra e condividi i dettagli del programma e della configurazione</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Sincronizza adesso</string>
<string name="account_synchronizing_now">Sincronizzazione in corso</string>
<string name="account_settings">Impostazioni account</string>
<string name="account_delete">Elimina account</string>
<string name="account_delete_confirmation_title">Cancellare l\'account?</string>
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">Permessi calendario</string>
<string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string>
<string name="permissions_contacts">Permessi Contatti</string>
<string name="permissions_contacts_request">Richiesta autorizzazione ai contatti</string>
<string name="permissions_opentasks">Permessi OpenTasks</string>
<string name="permissions_opentasks_request">Richiesta autorizzazione ad OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Aggiungi account</string>
<string name="login_email_address">Indirizzo email</string>
<string name="login_email_address_error">È necessario un indirizzo email valido</string>
<string name="login_password">Password</string>
<string name="login_password_required">Password richiesta</string>
<string name="login_back">Indietro</string>
<string name="login_configuration_detection">Rilevazione configurazione</string>
<string name="login_querying_server">Attendere, invio richiesta al server...</string>
<string name="login_view_logs">Vedi i log</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Impostazioni: %s</string>
<string name="settings_authentication">Autenticazione</string>
<string name="settings_password">Password</string>
<string name="settings_enter_password">Inserisci la tua password:</string>
<string name="settings_sync">Sincronizzazione</string>
<string name="settings_sync_interval_contacts">Intervallo sincr. Contatti</string>
<string name="settings_sync_summary_manually">Solo manualmente</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s e a seguito di ogni cambiamento locale</string>
<string name="settings_sync_summary_not_available">Non disponile</string>
<string name="settings_sync_interval_calendars">Intervallo sincr. calendari</string>
<string-array name="settings_sync_interval_names">
<item>Solo manualmente</item>
<item>Ogni 15 minuti</item>
<item>Ogni ora</item>
<item>Ogni 2 ore</item>
<item>Ogni 4 ore</item>
<item>Una volta al giorno</item>
</string-array>
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
<string name="settings_sync_wifi_only_ssid">Restrizione sul SSID del WiFi</string>
<string name="settings_sync_wifi_only_ssid_on">La sincronizzazione avverrà solo con %s</string>
<string name="settings_sync_wifi_only_ssid_off">Verranno usate tutte le connessioni WiFi</string>
<string name="settings_sync_wifi_only_ssid_message">Inserisci il nome di una rete WiFi (SSID) per limitare la sincronizzazione solo con questa rete o lasciare in bianco per usare tutte le connessioni WiFi.</string>
<!--collection management-->
<string name="create_addressbook">Crea indirizzario</string>
<string name="create_addressbook_display_name_hint">Il mio indirizzario</string>
<string name="create_collection_display_name_required">Il titolo è richiesto</string>
<string name="create_collection_description">Descrizione (opzionale):</string>
<string name="create_collection_create">Crea</string>
<string name="delete_collection_confirm_title">Sei sicuro/a\?</string>
<!--ExceptionInfoFragment-->
<string name="exception">Si è verificato un errore.</string>
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
<string name="exception_ioexception">Si è verificato un errore di I/O.</string>
<string name="exception_show_details">Mostra dettagli</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Informazioni di debug</string>
<string name="sync_error_calendar">Sincronizzazione del calendario fallita (%s)</string>
<string name="sync_error">Errore nel %s</string>
<string name="sync_error_http_dav">Errore del server nel %s</string>
<string name="sync_error_local_storage">Errore del database nel %s</string>
<!--cert4android-->
<string name="delete_collection_deleting_collection">Eliminazione della raccolta</string>
<string name="delete_collection">Elimina</string>
<string name="view_collection_import">Importa</string>
<string name="view_collection_edit">Modifica</string>
<string name="view_collection_members">Gestisci membri</string>
<string name="delete_collection_confirm_warning">Questa raccolta (%s) e tutti i suoi dati verranno rimossi dal server.</string>
</resources>

@ -5,62 +5,53 @@
~ 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
-->
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
--><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- common strings -->
<string name="app_name">EteSync</string>
<string name="account_title_address_book">EteSync adressebok</string>
<string name="account_title_address_book">EteSync-adressebok</string>
<string name="address_books_authority_title">Adressebøker</string>
<string name="help">Hjelp</string>
<string name="manage_accounts">Behandle kontoer</string>
<string name="please_wait">Vennligst vent …</string>
<string name="please_wait">Vent …</string>
<string name="send">Send</string>
<string name="notification_channel_crash_reports">Kræsjrapporter</string>
<string name="notification_channel_debugging">Debugging</string>
<string name="notification_channel_debugging">Avlusing</string>
<string name="notification_channel_general">Andre viktige meldinger</string>
<string name="notification_channel_sync">Synkronisering</string>
<string name="notification_channel_sync_errors">Synkroniseringsfeil</string>
<string name="notification_channel_sync_errors_desc">Viktige feil som stopper synkronisering, som uventede serversvar</string>
<string name="notification_channel_sync_errors_desc">Viktige feil som stopper synkronisering, som uventede tjenersvar</string>
<string name="notification_channel_sync_warnings">Synkroniseringsadvarsler</string>
<string name="notification_channel_sync_warnings_desc">Ikke-fatale synkroniseringsfeil, som avviste endringer i skrivebeskyttede samlinger</string>
<string name="notification_channel_sync_status">Statusmeldinger</string>
<string name="notification_channel_sync_status_desc">Informative statusmeldinger, som sammendrag av synkroniseringsendringer</string>
<!-- Crash -->
<string name="crash_title">EteSync har kræsjet!</string>
<string name="crash_message">Vennligst send feilmeldingen til utvikleren.</string>
<string name="crash_email_subject">EteSync debuginformasjon</string>
<string name="crash_email_body">Hvis mulig, ta med annen relevant informasjon, som f.eks. hva du gjorde for å utløse feilen.</string>
<string name="crash_message">Send feilmeldingen til utvikleren.</string>
<string name="crash_email_subject">EteSync-feilsporingsinfo</string>
<string name="crash_email_body">Hvis mulig, ta med annen relevant info, som f.eks. hva du gjorde for å utløse feilen.</string>
<!-- tourguide -->
<string name="tourguide_title">Visste du at?</string>
<!-- startup dialogs -->
<string name="startup_battery_optimization">Batterioptimalisering</string>
<string name="startup_battery_optimization_message">Android kan deaktivere/redusere EteSync-synkronisering etter noen dager. For å hindre dette, skru av batterioptimalisering.</string>
<string name="startup_battery_optimization_message">Android kan skru av/redusere EteSync-synkronisering etter noen dager. For å hindre dette, skru av batterioptimalisering.</string>
<string name="startup_battery_optimization_disable">Skru av for EteSync</string>
<string name="startup_dont_show_again">Ikke vis igjen</string>
<string name="startup_development_version">EteSync testversjon</string>
<string name="startup_development_version_message">Dette er en utviklerversjon av EteSync. Det kan hende at ting ikke virker som de skal. Send gjerne inn konstruktiv tilbakemelding for å forbedre EteSync.</string>
<string name="startup_development_version">EteSync-testversjon</string>
<string name="startup_development_version_message">Dette er en utviklerversjon av EteSync. Det kan hende at ting ikke virker som de skal. Send gjerne konstruktive tilbakemeldinger for å forbedre EteSync.</string>
<string name="startup_development_version_give_feedback">Gi tilbakemelding</string>
<string name="startup_vendor_specific_bugs">Potensielle leverandørfeil</string>
<string name="startup_vendor_specific_bugs_message">EteSync har oppdaget at du bruker en Android-versjon som kan inneholde leverandørspesifikke feil.\nSe ofte stilte spørsmål (FAQ) for mer informasjon.</string>
<string name="startup_vendor_specific_bugs_open_faq">Åpne FAQ</string>
<string name="startup_vendor_specific_bugs_message">EteSync har oppdaget at du bruker en Android-versjon som kan inneholde leverandørspesifikke feil.
\nSe ofte stilte spørsmål (O-S-S) for mer info.</string>
<string name="startup_vendor_specific_bugs_open_faq">Åpne O-S-S</string>
<!-- AboutActivity -->
<string name="about_license_terms">Lisensbetingelser</string>
<string name="about_license_info_no_warranty">Dette programmet kommer UTEN NOEN GARANTIER. Det et fri programvare, og du må gjerne distribuere det videre under visse betingelser.</string>
<!-- global settings -->
<string name="logging_davdroid_file_logging">EteSync-fillogging er PÅ</string>
<string name="logging_to_external_storage">Loggfører til ekstern lagring: %s</string>
<string name="logging_to_external_storage_warning">Deaktivering vil slette loggene</string>
<string name="logging_couldnt_create_file">Kunne ikke opprette ekstern loggfil: %s</string>
<string name="logging_no_external_storage">Ekstern lagring ikke funnet</string>
<!-- AccountsActivity -->
<string name="navigation_drawer_open">Åpne navigasjonsmeny</string>
<string name="navigation_drawer_close">Lukk navigasjonsmeny</string>
@ -70,50 +61,46 @@
<string name="navigation_drawer_external_links">Nyttige lenker</string>
<string name="navigation_drawer_website">Nettside</string>
<string name="navigation_drawer_faq">Ofte stilte spørsmål</string>
<string name="navigation_drawer_guide">Brukerguide</string>
<string name="navigation_drawer_guide">Brukerveiledning</string>
<string name="navigation_drawer_report_issue">Rapporter feil</string>
<string name="navigation_drawer_contact">Kontakt</string>
<string name="account_list_empty">Velkommen til EteSync!</string>
<string name="accounts_global_sync_disabled">Full automatisk synkronisering av systemet er deaktivert</string>
<string name="accounts_global_sync_enable">Aktiver</string>
<string name="accounts_showcase_add">Du må legge til en bruker for å kunne bruke EteSync. Klikk her for å legge til en...</string>
<string name="account_list_empty">Velkommen til EteSync.</string>
<string name="accounts_global_sync_disabled">Full automatisk synkronisering av systemet er avskrudd</string>
<string name="accounts_global_sync_enable">Skru på</string>
<string name="accounts_showcase_add">Du må legge til en bruker for å kunne bruke EteSync. Klikk her for å legge til en …</string>
<!-- AccountUpdateService -->
<!-- AppSettingsActivity -->
<string name="app_settings">Innstillinger</string>
<string name="app_settings_user_interface">Brukergrensesnitt</string>
<string name="app_settings_notification_settings">Varslingsinnstillinger</string>
<string name="app_settings_notification_settings_summary">Behandle varslingskanaler og deres innstillinger</string>
<string name="app_settings_reset_hints">Tilbakestill hint</string>
<string name="app_settings_reset_hints_summary">Gjenaktiverer hint som har blitt avvist tidligere</string>
<string name="app_settings_reset_hints_summary">Aktiverer tidligere avviste hint</string>
<string name="app_settings_reset_hints_success">Alle hint vil vises igjen</string>
<string name="app_settings_connection">Tilkobling</string>
<string name="app_settings_override_proxy">Overskriv proxyinnstillinger</string>
<string name="app_settings_override_proxy_on">Bruk egne proxyinnstillinger</string>
<string name="app_settings_override_proxy_off">Bruk systemets standard proxyinnstillinger</string>
<string name="app_settings_override_proxy_host">HTTP proxy-vertsnavn</string>
<string name="app_settings_override_proxy_port">HTTP proxy-port</string>
<string name="app_settings_override_proxy">Overskriv mellomtjenerinnstillinger</string>
<string name="app_settings_override_proxy_on">Bruk egne mellomtjenerinnstillinger</string>
<string name="app_settings_override_proxy_off">Bruk systemets forvalgte mellomtjenerinnstillinger</string>
<string name="app_settings_override_proxy_host">HTTP-mellomtjenervertsnavn</string>
<string name="app_settings_override_proxy_port">HTTP-mellomtjenerport</string>
<string name="app_settings_security">Sikkerhet</string>
<string name="app_settings_distrust_system_certs">Ikke klarer systemsertifikater</string>
<string name="app_settings_distrust_system_certs_on">Ikke klarer system- og brukersertifikater</string>
<string name="app_settings_distrust_system_certs_off">Klarer system- og brukersertifikater (anbefalt)</string>
<string name="app_settings_reset_certificates">Tilbakestill (ikke) klarerte sertifikater</string>
<string name="app_settings_reset_certificates">Tilbakestill (u-) klarerte sertifikater</string>
<string name="app_settings_reset_certificates_summary">Tilbakestiller klareringen av alle egne sertifikater</string>
<string name="app_settings_reset_certificates_success">Klareringen av alle egne sertifikater har blitt tilbakestilt</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_debug">Avlusing</string>
<string name="app_settings_log_to_external_storage">Loggfør til fil</string>
<string name="app_settings_log_to_external_storage_on">Deaktiver for å skjule varselet og slette loggene</string>
<string name="app_settings_log_to_external_storage_off">Aktiver for å loggføre meldinger i en fil</string>
<string name="app_settings_log_to_external_storage_on">Skru av for å skjule varselet og slette loggene</string>
<string name="app_settings_log_to_external_storage_off">Skru på for å loggføre meldinger til fil</string>
<string name="app_settings_log_verbose">Detaljert loggføring</string>
<string name="app_settings_log_verbose_summary">Detaljerte logger kan inneholde personlig informasjon</string>
<string name="app_settings_show_debug_info">Vis debuginformasjon</string>
<string name="app_settings_show_debug_info_details">Vis/del programvare- og konfigurasjonsdetaljer</string>
<string name="app_settings_log_verbose_summary">Detaljerte logger kan inneholde personlig info</string>
<string name="app_settings_show_debug_info">Vis feilsporingsinfo</string>
<string name="app_settings_show_debug_info_details">Vis/del programvare- og oppsettsdetaljer</string>
<string name="app_settings_force_language">Tving språk</string>
<string name="app_settings_force_language_default">Auto</string>
<string name="app_settings_change_notification">Vis endringsvarsler</string>
<!-- AccountActivity -->
<string name="account_synchronize_now">Synkroniser nå</string>
<string name="account_synchronizing_now">Synkroniserer nå</string>
@ -121,14 +108,12 @@
<string name="account_delete">Fjern konto</string>
<string name="account_show_fingerprint">Vis fingeravtrykk</string>
<string name="account_delete_confirmation_title">Vil du virkelig fjerne kontoen?</string>
<string name="account_delete_confirmation_text">Alle lokale kopier av adressebøker, kalendre og oppgavelister vil bli slettet.</string>
<string name="account_delete_confirmation_text">Alle lokale kopier av adressebøker, kalendre og gjøremålslister vil bli slettet.</string>
<string name="account_delete_collection_last_title">Kunne ikke slette siste samling</string>
<string name="account_delete_collection_last_text">Sletting av den siste samlingen tillates ikke, vennligst lag en ny samling hvis du vil slette denne.</string>
<string name="account_showcase_view_collection">Du kan klikke på et element for å vise samlingen. Derfra kan du se journalen, importere, og mye mer...</string>
<string name="account_click_install_tasks">Klikk her for å installere OpenTasks!</string>
<string name="account_delete_collection_last_text">Sletting av den siste samlingen tillates ikke, lag en ny samling hvis du vil slette denne.</string>
<string name="account_showcase_view_collection">Du kan klikke på et element for å vise samlingen. Derfra kan du vise journalen, importere, og mye mer...</string>
<string name="account_click_install_tasks">Klikk her for å installere OpenTasks.</string>
<string name="show_fingperprint_title">Mitt fingeravtrykk</string>
<!-- ViewCollection -->
<string name="change_journal_title">Endre journal</string>
<string name="account_showcase_import">For å importere kontakter og kalendre til Etesync, må du klikke på menyen og velge \"Importer\".</string>
@ -136,23 +121,22 @@
<string name="members_owner_only">Bare eieren av denne samlingen (%s) har lov til å se dens medlemmer.</string>
<string name="not_allowed_title">Ikke tillatt</string>
<string name="edit_owner_only">Bare eieren av denne samlingen (%s) har lov til å redigere den.</string>
<string name="members_old_journals_not_allowed">Deling av journaler av gammel stil er ikke tillatt. For å dele denne journalen, lag en ny journal og kopier innholder i den over ved hjelp av \"importer\"-dialogen. Hvis du opplever problemer, kontakt kundeservice.</string>
<string name="members_old_journals_not_allowed">Deling av journaler i gammel stil tillates ikke. For å dele denne journalen, lag en ny og kopier innholdet i den over ved hjelp av \"importer\"-dialogen. Hvis du opplever problemer, kontakt brukerstøtte.</string>
<!-- CollectionMembers -->
<string name="collection_members_title">Medlemmer</string>
<string name="collection_members_list_loading">Laster inn medlemmer...</string>
<string name="collection_members_list_loading">Laster inn medlemmer</string>
<string name="collection_members_list_empty">Ingen medlemmer</string>
<string name="collection_members_add">Legg til medlem</string>
<string name="collection_members_add_error">Feil oppstod under tillegging av medlem</string>
<string name="collection_members_adding">Legger til medlem</string>
<string name="trust_fingerprint_title">Kontroller sikkerhetsfingeravtrykk</string>
<string name="trust_fingerprint_body">Kontroller %ss fingeravtrykk for å bekrefte at krypteringen er sikker.</string>
<string name="collection_members_error_user_not_found">Bruker (%s) finnes ikke, eller har skrudd av journaldeling.</string>
<string name="trust_fingerprint_body">Kontroller %s sitt fingeravtrykk for å bekrefte at krypteringen er sikker.</string>
<string name="collection_members_error_user_not_found">Brukeren (%s) finnes ikke, eller har skrudd av journaldeling.</string>
<string name="collection_members_removing">Fjerner medlem</string>
<string name="collection_members_remove_error">Feil opppstod under fjerning av medlem</string>
<string name="collection_members_remove_error">Kunne ikke fjerne medlem</string>
<string name="collection_members_remove_title">Fjern medlem</string>
<string name="collection_members_remove">Vil du fjerne %ss tilgang?\nVær klar over at en ondsinnet bruker potentsielt kan beholde tilgang til krypteringsnøkler. Se ofte stilte spørsmål for mer informasjon.</string>
<string name="collection_members_remove">Vil du fjerne %s sin tilgang\?
\nVær klar over at en ondsinnet bruker potensielt kan beholde tilgang til krypteringsnøkler. Se ofte stilte spørsmål for mer info.</string>
<!-- JournalItemActivity -->
<string name="about">Om</string>
<string name="journal_item_tab_main">Forside</string>
@ -160,7 +144,7 @@
<string name="journal_item_attendees">Deltakere</string>
<string name="journal_item_reminders">Påminnelser</string>
<string name="journal_item_phone">Telefon</string>
<string name="journal_item_email">Epost</string>
<string name="journal_item_email">E-post</string>
<string name="journal_item_organization">Organisasjon</string>
<string name="journal_item_job_description">Jobbeskrivelse</string>
<string name="journal_item_impp">Nettprat</string>
@ -175,8 +159,6 @@
<string name="journal_item_member_count">Antall medlemmer</string>
<string name="journal_item_restore_action">Gjenopprett tilstand</string>
<string name="journal_item_restore_dialog_body">Gjenopprettet elementet til forrige tilstand.</string>
<!-- PermissionsActivity -->
<string name="permissions_title">EteSync-tilganger</string>
<string name="permissions_calendar">Kalendertilgang</string>
@ -186,20 +168,20 @@
<string name="permissions_contacts_details">For å synkronisere adressebøker med de lokale kontaktlistene dine, trenger EteSync tilgang til kontaktene dine.</string>
<string name="permissions_contacts_request">Be om kontakttilgang</string>
<string name="permissions_opentasks">OpenTasks-tilgang</string>
<string name="permissions_opentasks_details">For å synkronisere oppgaver med de lokale oppgavelistene dine, trenger EteSync tilgang til OpenTasks.</string>
<string name="permissions_opentasks_details">For å synkronisere gjøremål med de lokale gjøremålslistene dine, trenger EteSync tilgang til OpenTasks.</string>
<string name="permissions_opentasks_request">Be om OpenTasks-tilgang</string>
<!-- AddAccountActivity -->
<string name="login_title">Legg til konto</string>
<string name="login_email_address">Epost</string>
<string name="login_email_address_error">Gyldig epostadresse kreves</string>
<string name="login_email_address">E-post</string>
<string name="login_email_address_error">Gyldig e-postadresse kreves</string>
<string name="login_password">Passord</string>
<string name="login_custom_server">EteSync-server-URL</string>
<string name="login_custom_server_error">Ugyldig URL finnet, glemte du å ta med https://?</string>
<string name="login_custom_server">EteSync-tjenernettadresse</string>
<string name="login_custom_server_error">Ugyldig nettadresse finnet, glemte du å ta med https://\?</string>
<string name="login_toggle_advanced">Vis avanserte innstillinger</string>
<string name="login_encryption_password">Krypteringspassord</string>
<string name="login_encryption_check_password">* Vennligst dobbeltsjekk passordet, da det ikke kan gjenopprettes hvis det er feil!</string>
<string name="login_encryption_extra_info">Dette passordet bruker til å kryptere dataen din, i motsetning til det forrige, som brukes til å logge inn på tjenesten.\nDu bes om å sette et separat krypteringspassord av sikkerhetsårsaker. For mer informasjon, se ofte stilte spørsmål her: %s</string>
<string name="login_encryption_check_password">* Dobbeltsjekk passordet, siden det ikke kan gjenopprettes hvis det er feil!</string>
<string name="login_encryption_extra_info">Dette passordet bruker til å kryptere dataen din, i motsetning til det forrige, som brukes til å logge inn på tjenesten.
\nDu bes om å sette et eget krypteringspassord av sikkerhetsgrunner. For mer info, sjekk ofte stilte spørsmål her: %s</string>
<string name="login_password_required">Passord kreves</string>
<string name="login_login">Logg inn</string>
<string name="login_signup">Registrer deg</string>
@ -210,44 +192,40 @@
<string name="login_encryption_account_label">Konto:</string>
<string name="login_service_details_description">Dette er innloggingspassordet ditt, *ikke* krypteringspassordet!</string>
<string name="login_forgot_password">Glemt passord?</string>
<string name="login_configuration_detection">Oppdag konfigurasjon</string>
<string name="login_querying_server">Vennligst vent, kontakter serveren…</string>
<string name="login_configuration_detection">Oppdag oppsett</string>
<string name="login_querying_server">Vent, kontakter tjeneren …</string>
<string name="login_wrong_username_or_password">Kunne ikke logge inn (sannsynligvis feil brukernavn eller passord).\nHar du registrert deg?</string>
<string name="login_view_logs">Vis logger</string>
<string name="login_encryption_setup_title">Setter opp kryptering</string>
<string name="login_encryption_setup">Vennligst vent, setter opp kryptering…</string>
<string name="setting_up_encryption">Setter opp kryptering</string>
<string name="setting_up_encryption_content">Vennligst vent, setter opp kryptering…</string>
<string name="account_creation_failed">Kunne ikke opprette konto</string>
<string name="wrong_encryption_password">Feil krypteringspassord</string>
<string name="wrong_encryption_password_content">Mottok en integritetsfeil ved tilgang til kontoen din, noe som mest sannsynlig skyldes at du skrev inn feil krypteringspassord.\nVennligst merk at brukernavnet skiller store og små bokstaver, så prøv gjerne andre kombinasjoner, skriv for eksempel første bokstav som stor bokstav.\n\Feil: %s</string>
<string name="wrong_encryption_password_content">Mottok en integritetsfeil ved tilgang til kontoen din, noe som mest sannsynlig skyldes at du skrev inn feil krypteringspassord.
\nMerk at brukernavnet skiller mellom store og små bokstaver, så prøv gjerne andre kombinasjoner, skriv for eksempel første bokstav som stor bokstav.
\neil: %s</string>
<!-- ChangeEncryptionPasswordActivity -->
<string name="change_encryption_password_title">Endre krypteringspassord</string>
<string name="change_encryption_password_extra_info">Vennligst ikke bruk dette verktøyet dersom du mistenker at krypteringspassordet ditt har blitt kompromittert. Kontakt kundeservice i stedet.</string>
<string name="change_encryption_password_extra_info">Ikke bruk dette verktøyet dersom du mistenker at krypteringspassordet ditt har blitt kompromittert. Kontakt brukerstøtte i stedet.</string>
<string name="change_encryption_password_new_password">Nytt krypteringspassord</string>
<string name="change_encryption_password_are_you_sure">Er du sikker på at du vil fortsette? Denne prosessen kan ikke stoppes halvveis eller angres, og kan ta en stund å fullføre.</string>
<string name="change_encryption_password_success_title">Endring av krypteringspassord</string>
<string name="change_encryption_password_success_body">Krypteringspassordet ble endret!</string>
<!-- SetupUserInfoFragment -->
<string name="login_user_info_error_title">Feil ved setting av brukerinfo</string>
<!-- ImportFragment -->
<string name="import_dialog_title">Import</string>
<string name="import_dialog_failed_title">Import mislyktes</string>
<string name="import_dialog_failed_body">Grunn: %s\nRapporter til utviklerne?</string>
<string name="import_dialog_loading_file">Laster fil (kan ta en stund)...</string>
<string name="import_dialog_adding_entries">Legger til elementer...</string>
<string name="import_dialog_loading_file">Laster fil (kan ta en stund)</string>
<string name="import_dialog_adding_entries">Legger til elementer</string>
<string name="import_dialog_success">Behandlet %1$d elementer.\nLagt til: %2$d\nEndret: %3$d\nHoppet over (mislyktes): %4$d</string>
<string name="import_permission_required">Tilgang til enhetslagring kreves for import.</string>
<string name="choose_file">Velg fil</string>
<!-- AccountSettingsActivity -->
<string name="settings_title">Innstillinger: %s</string>
<string name="settings_manage_account">Behandle konto</string>
<string name="settings_account_dashboard">Kontodashbord</string>
<string name="settings_account_dashboard">Konto-oversikt</string>
<string name="settings_manage_account_summary">Endre betalingsinfo, plan og andre kontoinnstillinger</string>
<string name="settings_encryption">Kryptering</string>
<string name="settings_password">Innloggingspassord</string>
@ -269,30 +247,29 @@
<item>Hver 4. time</item>
<item>Daglig</item>
</string-array>
<string name="settings_sync_wifi_only">Bare synkroniser over WiFi</string>
<string name="settings_sync_wifi_only_on">Synkronisering er begrenset til WiFi-forbindelser</string>
<string name="settings_sync_wifi_only">Kun synkroniser over Wi-Fi</string>
<string name="settings_sync_wifi_only_on">Synkronisering er begrenset til Wi-Fi-forbindelser</string>
<string name="settings_sync_wifi_only_off">Tilkoblingstype tas ikke hensyn til</string>
<string name="settings_sync_wifi_only_ssid">WiFi-SSID-begrensninv</string>
<string name="settings_sync_wifi_only_ssid_on">Vil bare synkronisere på %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle WiFi-tilkoblinger kan brukes</string>
<string name="settings_sync_wifi_only_ssid_message">Skriv inn navnet på et WiFi-nettverk (SSID) for å begrense synkronisering til dette nettverket, eller la stå blankt for å tillate alle WiFi-forbindelser.</string>
<string name="settings_sync_wifi_only_ssid">Wi-Fi-SSID-begrensning</string>
<string name="settings_sync_wifi_only_ssid_on">Vil kun synkronisere på %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle Wi-Fi-tilkoblinger kan brukes</string>
<string name="settings_sync_wifi_only_ssid_message">Skriv inn navnet på et Wi-Fi-nettverk (SSID) for å begrense synkronisering til dette nettverket, eller la stå blankt for å tillate alle Wi-Fi-forbindelser.</string>
<string name="settings_carddav">Kontakter</string>
<string name="settings_caldav">Kalender</string>
<string name="settings_taskdav">Oppgaver</string>
<string name="settings_taskdav">Gjøremål</string>
<!-- collection management -->
<string name="create_addressbook">Opprett adressebok</string>
<string name="create_addressbook_display_name_hint">Min adressebok</string>
<string name="create_calendar">Opprett kalender</string>
<string name="create_calendar_display_name_hint">Min kalender</string>
<string name="create_tasklist">Opprett oppgaveliste</string>
<string name="create_tasklist_display_name_hint">Min oppgaveliste</string>
<string name="create_tasklist">Opprett gjøremålsliste</string>
<string name="create_tasklist_display_name_hint">Min gjøremålsliste</string>
<string name="install_opentasks">Installer OpenTasks</string>
<string name="edit_collection">Endre samling</string>
<string name="create_collection_color">Sett samlingens farge</string>
<string name="create_collection_creating">Oppretter samling</string>
<string name="create_collection_display_name">Visningsnavn (tittel) på denne samlingen:</string>
<string name="create_collection_display_name_required">Tittel er påkrevd</string>
<string name="create_collection_display_name_required">Tittel kreves</string>
<string name="create_collection_description">Beskrivelse (valgfritt):</string>
<string name="view_collection_edit">Endre</string>
<string name="view_collection_import">Importer</string>
@ -300,31 +277,28 @@
<string name="create_collection_create">Lagre</string>
<string name="delete_collection">Slett</string>
<string name="delete_collection_confirm_title">Er du sikker?</string>
<string name="delete_collection_confirm_warning">Denne samlingen (%s) og alle dens data vil slettes fra serveren.</string>
<string name="delete_collection_confirm_warning">Denne samlingen (%s) og alle dens data vil slettes fra tjeneren.</string>
<string name="delete_collection_deleting_collection">Sletter samling</string>
<!-- JournalViewer -->
<string name="journal_entries_list_empty">Journalen er tom.\n(Kanskje den fortsatt synkroniserer?)</string>
<string name="journal_entries_loading">Laster journalelementer...</string>
<string name="journal_entries_loading">Laster inn journalelementer …</string>
<!-- ExceptionInfoFragment -->
<string name="exception">En feil har oppstått.</string>
<string name="exception_httpexception">En HTTP-feil har oppstått.</string>
<string name="exception_ioexception">En I/O-feil har oppstått.</string>
<string name="exception_show_details">Vis detaljer</string>
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Debuginformasjon</string>
<string name="debug_info_more_data_shared">Når du klikker på del vil epost-appen åpne med dataene under, i tillegg til noe ekstra debuginformasjon, vedlagt. Det kan kanskje inneholde sensitiv informasjon, så vennligst se igjennom dataene før du sender.</string>
<string name="debug_info_title">Feilsporingsinfo</string>
<string name="debug_info_more_data_shared">Når du klikker på del vil e-postprogrammet åpnes med dataene under, i tillegg til noe ekstra feilsporingsinfo, vedlagt. Det kan kanskje inneholde sensitiv info, så sjekk alt før du sender.</string>
<string name="sync_error_permissions">EteSync-tillatelser</string>
<string name="sync_error_permissions_text">Ekstra tillatelser kreves</string>
<string name="sync_error_calendar">Kalendersynkronisering mislyktes (%s)</string>
<string name="sync_error_contacts">Kontaktsynkronisering mislyktes (%s)</string>
<string name="sync_error_tasks">Oppgavesynkronisering mislyktes (%s)</string>
<string name="sync_error_tasks">Gjøremålssynkronisering mislyktes (%s)</string>
<string name="sync_error">Feil ved %s</string>
<string name="sync_error_integrity">Integritetsfeil ved %s</string>
<string name="sync_error_http_dav">Serverfeil ved %s</string>
<string name="sync_error_unavailable">Kunne ikke koble til serveren ved %s</string>
<string name="sync_error_http_dav">Tjenerfeil ved %s</string>
<string name="sync_error_unavailable">Kunne ikke koble til tjenern ved %s</string>
<string name="sync_error_local_storage">Databasefeil ved %s</string>
<string name="sync_error_journal_readonly">Journalen er skrivebeskyttet</string>
<string name="sync_phase_prepare">forberedelse av synkronisering</string>
@ -337,43 +311,55 @@
<string name="sync_phase_apply_local_entries">gjentillegging av lokale elementer</string>
<string name="sync_phase_push_entries">sending av elementer</string>
<string name="sync_phase_post_processing">etterbehandling</string>
<string name="sync_error_unauthorized">Autentisering mislyktes</string>
<string name="sync_error_unauthorized">Innlogging mislyktes</string>
<string name="sync_error_user_inactive">Brukeren er inaktiv</string>
<string name="sync_successfully_calendar" formatted="false">Kalender \"%s\" endret (%s)</string>
<string name="sync_successfully_calendar" formatted="false">Kalender «%s» endret (%s)</string>
<string name="sync_successfully_contacts" formatted="false">Kontakter endret (%s)</string>
<string name="sync_successfully_tasks" formatted="false">Oppgavene \"%s\" endret (%s)</string>
<string name="sync_successfully_tasks" formatted="false">Gjøremålene «%s» er endret (%s)</string>
<string name="sync_successfully_modified" formatted="false">%s endret.</string>
<string name="sync_successfully_modified_full" formatted="false">%s lagt til.\n%s oppdatert.\n%s slettet.</string>
<string name="sync_successfully_modified_full" formatted="false">%s oppdatert.\n%s slettet.</string>
<string name="sync_journal_readonly">Journalen \"%s\" er skrivebeskyttet</string>
<string name="sync_journal_readonly_message">Journalen er skrivebeskyttet, så alle endringene dine (%d) har blitt omgjort.</string>
<!-- Calendar invites -->
<string name="sync_calendar_attendees_notification_title" formatted="false">%s</string>
<string name="sync_calendar_attendees_notification_content">Send invitasjoner til gjester?</string>
<string name="calendar_attendees_send_email_action">Send invitasjoner</string>
<string name="sync_calendar_attendees_email_subject" formatted="false">Invitasjon: %s @ %s</string>
<string name="sync_calendar_attendees_email_content" formatted="false">Du har blitt invitert til følgende hendelse:\n\n%s\nNår: %s\nHvor: %s\nDeltakere: %s\n\n--\nSendt med EteSync - sikker, ende-til-ende-kryptert og privat synkronisering.\nFå det på https://www.EteSync.com</string>
<!-- cert4android -->
<string name="certificate_notification_connection_security">EteSync: Tilkoblingssikkerhet</string>
<string name="trust_certificate_unknown_certificate_found">EteSync har mottatt et ukjent sertifikat. Ønsker du å klarere det?</string>
<!-- WebView activity-->
<string name="loading">Laster...</string>
<string name="loading">Laster</string>
<string name="loading_error_title">Innlastingsfeil</string>
<string name="loading_error_content">Oppdater</string>
<string name="open_url_no_activity">Kunne ikke åpne link: linken støttes ikke!</string>
<string name="open_url_no_activity">Kunne ikke åpne lenke: Lenken støttes ikke!</string>
<!-- Import Activity -->
<string name="import_button_file">Fra fil</string>
<string name="import_button_local">Fra konto</string>
<string name="import_select_account">Velg konto</string>
<!-- Event (from Etar) -->
<string name="event_info_organizer">Arrangør:</string>
<!-- Notifications -->
<string name="notification_channel_name">EteSync</string>
<string name="notification_channel_description">EteSync-varsler</string>
<string name="install_tasksorg">Installer Tasks.org</string>
<string name="import_account_top_notice">Velg en konto å importere alle dens oppføringer fra.</string>
<string name="login_encryption_enter_password">Du er innlogget som «%s». Skriv inn ditt krypteringspassord for å fortsette, eller logg ut fra sidemenyen.</string>
<string name="login_encryption_set_new_password">Sett ditt krypteringspassord nedenfor, og forsikre deg om at det er riktig, siden det *ikke* kan gjenopprettes!</string>
<string name="permissions_tasks_org_request">Forespør tilgang til Tasks.org</string>
<string name="permissions_tasks_org_details">For å synkronisere gjøremål med din lokale gjøremålsliste, trenger EteSync tilgang til Tasks.org.</string>
<string name="permissions_tasks_org">Tasks.org-tilganger</string>
<string name="use_native_apps_body">EteSync integrerer seg sømløst med Android, slik at du kan bruke eksisterende kontaktliste og kalenderprogrammer.
\n
\nFor mer info, sjekk ut brukerveiledningen.</string>
<string name="use_native_apps_title">Visste du at\?</string>
<string name="tasks_not_showing">* Loggføring vises kun hvis en gjøremålstilbyder er installert.</string>
<string name="account_tasks_not_showing">* Synkroniserer ikke gjøremål (installer en tilbyder)</string>
<string name="app_settings_prefer_tasksorg_snack">Det kan hende du må fjerne og legge til kontoen din igjen før endringene trer i effekt.</string>
<string name="app_settings_prefer_tasksorg_off">Synkroniserer gjøremål med OpenTasks hvis tilgjengelig</string>
<string name="app_settings_prefer_tasksorg_on">Synkroniserer gjøremål med Tasks.org hvis tilgjengelig</string>
<string name="app_settings_prefer_tasksorg">Foretrekk Tasks.org-gjøremålstilbyder</string>
<string name="app_settings_sync">Synkroniser</string>
<string name="accounts_missing_permissions">Manglende tilganger: %s</string>
</resources>

@ -179,8 +179,8 @@
<string name="login_wrong_username_or_password">Nie można zweryfikować (prawdopodobnie zła nazwa użytkownika lub hasło).</string>
<string name="login_view_logs">Zobacz logi</string>
<string name="login_encryption_setup_title">Ustawianie Szyfrowania</string>
<string name="login_encryption_setup">Proszę czekać, ustawianie szyfrowania…</string>
<string name="setting_up_encryption">Ustawianie Szyfrowania</string>
<string name="setting_up_encryption_content">Proszę czekać, ustawianie szyfrowania…</string>
<string name="account_creation_failed">Tworzenie konta nie powiodło się</string>
@ -287,7 +287,7 @@
<string name="sync_successfully_calendar" formatted="false">Kalendarz \"%s\" został zmodyfikowany (%s)</string>
<string name="sync_successfully_contacts" formatted="false">Kontakty zostały zmodyfikowane (%s)</string>
<string name="sync_successfully_modified" formatted="false">%s został zmodyfikowany.</string>
<string name="sync_successfully_modified_full" formatted="false">%s został dodany. \n%s został zaktualizowany.\n%s został usunięty.</string>
<string name="sync_successfully_modified_full" formatted="false">%s został zaktualizowany.\n%s został usunięty.</string>
<!-- cert4android -->
<string name="certificate_notification_connection_security">EteSync: Bezpieczeństwo połączenia</string>

@ -1,42 +1,59 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="help">Помощь</string>
<string name="send">Отправить</string>
<!--startup dialogs-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<!--AccountActivity-->
<!--PermissionsActivity-->
<!--AddAccountActivity-->
<!--AccountSettingsActivity-->
<string name="settings_authentication">Идентификация</string>
<string name="settings_password">Пароль</string>
<string name="settings_enter_password">Введите свой пароль:</string>
<string name="settings_sync">Синхронизация</string>
<string name="settings_sync_interval_contacts">Интервал синхронизации контактов</string>
<string name="settings_sync_summary_manually">Вручную</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s и немедленно при локальных изменениях</string>
<string name="settings_sync_summary_not_available">Недоступно</string>
<string name="settings_sync_interval_calendars">Период синхронизации календарей</string>
<string-array name="settings_sync_interval_names">
<item>Вручную</item>
<item>Каждые 15 минут</item>
<item>Каждый час</item>
<item>Каждые 2 часа</item>
<item>Каждые 4 часа</item>
<item>Раз в сутки</item>
</string-array>
<!--collection management-->
<!--ExceptionInfoFragment-->
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Отладочная информация</string>
<string name="sync_error_calendar">Синхронизация календаря завершена с ошибкой (%s)</string>
<string name="sync_error">Ошибка %s</string>
<string name="sync_error_http_dav">Ошибка сервера %s</string>
<string name="sync_error_local_storage">Ошибка базы данных в процессе %s</string>
<!--cert4android-->
</resources>
<!--common strings-->
<string name="help">Помощь</string>
<string name="send">Отправить</string>
<!--startup dialogs-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<!--AccountActivity-->
<!--PermissionsActivity-->
<!--AddAccountActivity-->
<!--AccountSettingsActivity-->
<string name="settings_authentication">Идентификация</string>
<string name="settings_password">Пароль</string>
<string name="settings_enter_password">Введите свой пароль:</string>
<string name="settings_sync">Синхронизация</string>
<string name="settings_sync_interval_contacts">Интервал синхронизации контактов</string>
<string name="settings_sync_summary_manually">Вручную</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s и немедленно при локальных изменениях</string>
<string name="settings_sync_summary_not_available">Недоступно</string>
<string name="settings_sync_interval_calendars">Период синхронизации календарей</string>
<string-array name="settings_sync_interval_names">
<item>Вручную</item>
<item>Каждые 15 минут</item>
<item>Каждый час</item>
<item>Каждые 2 часа</item>
<item>Каждые 4 часа</item>
<item>Раз в сутки</item>
</string-array>
<!--collection management-->
<!--ExceptionInfoFragment-->
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Отладочная информация</string>
<string name="sync_error_calendar">Синхронизация календаря завершена с ошибкой (%s)</string>
<string name="sync_error">Ошибка %s</string>
<string name="sync_error_http_dav">Ошибка сервера %s</string>
<string name="sync_error_local_storage">Ошибка базы данных в процессе %s</string>
<!--cert4android-->
<string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="logging_no_external_storage">Внешний накопитель не обнаружен</string>
<string name="about_license_terms">Пользовательское соглашение</string>
<string name="startup_vendor_specific_bugs_open_faq">Открыть FAQ</string>
<string name="startup_development_version_give_feedback">Оставить отзыв</string>
<string name="startup_development_version">Предварительная версия EteSync</string>
<string name="startup_dont_show_again">Больше не показывать</string>
<string name="startup_battery_optimization">Оптимизация батареи</string>
<string name="tourguide_title">Знаете ли вы\?</string>
<string name="notification_channel_sync_errors">Ошибки синхронизации</string>
<string name="notification_channel_sync">Синхронизация</string>
<string name="notification_channel_general">Другие важные сообщения</string>
<string name="notification_channel_debugging">Отладка</string>
<string name="please_wait">Пожалуйста, подождите …</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="address_books_authority_title">Адресные книги</string>
<string name="app_name">EteSync</string>
</resources>

@ -38,8 +38,8 @@
<string name="app_settings_show_debug_info">Hata ayıklama bilgilerini göster</string>
<string name="app_settings_show_debug_info_details">Yazılım ve konfigürasyon detaylarına bak/paylaş</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Şimdi senkronize et</string>
<string name="account_synchronizing_now">Senkronize ediyor</string>
<string name="account_synchronize_now">Şimdi eşzamanla</string>
<string name="account_synchronizing_now">Şimdi eşzamanlanıyor</string>
<string name="account_settings">Hesap ayarları</string>
<string name="account_delete">Hesabı sil</string>
<string name="account_delete_confirmation_title">Hesap gerçekten silinsin mi?</string>
@ -54,7 +54,7 @@
<!--AddAccountActivity-->
<string name="login_title">Hesap ekle</string>
<string name="login_email_address">Eposta adresi</string>
<string name="login_email_address_error">Geçerli eposta adresi zorunludur</string>
<string name="login_email_address_error">Geçerli bir e-posta adresi gerekli</string>
<string name="login_password">Parola</string>
<string name="login_password_required">Parola zorunludur</string>
<string name="login_back">Geri</string>
@ -66,7 +66,7 @@
<string name="settings_authentication">Doğrulama</string>
<string name="settings_password">Parola</string>
<string name="settings_enter_password">Parola girin:</string>
<string name="settings_sync">Senkronizasyon</string>
<string name="settings_sync">Eşzamanlama</string>
<string name="settings_sync_interval_contacts">Kişiler senk. aralığı</string>
<string name="settings_sync_summary_manually">Sadece elle</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">%s + yerel değişikliklerde hemen</string>
@ -80,13 +80,13 @@
<item>Her 4 saatte bir</item>
<item>Günde bir</item>
</string-array>
<string name="settings_sync_wifi_only">Sadece WiFi üzerinden senkronize et</string>
<string name="settings_sync_wifi_only_on">Senkronizasyon WiFi bağlantıları ile kısıtlıdır</string>
<string name="settings_sync_wifi_only">Sadece WiFi üzerinden eşzamanla</string>
<string name="settings_sync_wifi_only_on">Eşzamanlama WiFi bağlantıları ile sınırlı</string>
<string name="settings_sync_wifi_only_off">Bağlantı tipi göz önünde bulundurulmaz</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID kısıtlaması</string>
<string name="settings_sync_wifi_only_ssid_on">Sadece %s üzerinden senkronize olur</string>
<string name="settings_sync_wifi_only_ssid_on">Sadece %s üzerinden eşzamanlanacak</string>
<string name="settings_sync_wifi_only_ssid_off">Tüm WiFi bağlantıları kullanılabilir</string>
<string name="settings_sync_wifi_only_ssid_message">Senkronizasyonu sadece bir WiFi ağına kısıtlamak için bu ağın adını (SSID) gir, veya tüm WiFi bağlantıları için boş bırak.</string>
<string name="settings_sync_wifi_only_ssid_message">Eşzamanlamayı sadece bir WiFi ağına kısıtlamak için bu ağın adını (SSID) girin, veya tüm WiFi bağlantıları için boş bırakın.</string>
<!--collection management-->
<string name="create_addressbook">Rehber yarat</string>
<string name="create_addressbook_display_name_hint">Benim Rehberim</string>
@ -106,7 +106,7 @@
<string name="exception_show_details">Detayları göster</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Hata ayıklama bilgisi</string>
<string name="sync_error_calendar">Takvim senkronizasyonu başarısız (%s)</string>
<string name="sync_error_calendar">Takvim eşzamanlama başarısız oldu (%s)</string>
<string name="sync_error">%s yaparken hata</string>
<string name="sync_error_http_dav">%s yaparken sunucu hatası</string>
<string name="sync_error_local_storage">%s yaparken veritabanı hatası</string>
@ -120,4 +120,262 @@
<string name="account_title_address_book">EteSync Adres Defteri</string>
<string name="app_name">EteSync</string>
<string name="notification_channel_sync_errors_desc">Beklenmeyen sunucu yanıtları gibi eşzamanlamayı durduran önemli hatalar</string>
<string name="login_wrong_username_or_password">Kimlik doğrulanamadı (muhtemelen yanlış kullanıcı adı veya parola).
\nKaydoldunuz mu\?</string>
<string name="login_signup">Kaydol</string>
<string name="notification_channel_description">EteSync bildirimleri</string>
<string name="notification_channel_name">EteSync</string>
<string name="event_info_organizer">Düzenleyen:</string>
<string name="import_select_account">Hesap Seç</string>
<string name="import_button_local">Hesaptan</string>
<string name="import_button_file">Dosyadan</string>
<string name="open_url_no_activity">Bağlantıılamadı: bağlantı desteklenmiyor!</string>
<string name="loading_error_content">Yenile</string>
<string name="loading_error_title">Yükleme Hatası</string>
<string name="loading">Yükleniyor...</string>
<string name="trust_certificate_unknown_certificate_found">EteSync bilinmeyen bir sertifikayla karşılaştı. Güvenmek ister misiniz\?</string>
<string name="certificate_notification_connection_security">EteSync: Bağlantı güvenliği</string>
<string name="sync_calendar_attendees_email_content" formatted="false">Aşağıdaki etkinliğe davet edildiniz:
\n
\n%s
\nNe zaman: %s
\nNerede: %s
\nKatılanlar: %s
\n
\n--
\nEteSync ile gönderildi - güvenli, uçtan uca şifreli ve özel veri eşzamanlama.
\nhttps://www.EteSync.com adresinden edinin</string>
<string name="sync_calendar_attendees_email_subject" formatted="false">Davet: %s @ %s</string>
<string name="calendar_attendees_send_email_action">Davet gönder</string>
<string name="sync_calendar_attendees_notification_content">Misafirlere davet gönderilsin mi\?</string>
<string name="sync_calendar_attendees_notification_title" formatted="false">%s</string>
<string name="sync_journal_readonly_message">Günlük salt okunur olduğundan tüm değişiklikleriniz (%d) geri alındı.</string>
<string name="sync_journal_readonly">Günlük \"%s\" salt okunur</string>
<string name="sync_successfully_modified_full" formatted="false">%s güncellendi.
\n%s silindi.</string>
<string name="sync_successfully_modified" formatted="false">%s değiştirildi.</string>
<string name="sync_successfully_tasks" formatted="false">Görevler \"%s\" değiştirildi (%s)</string>
<string name="sync_successfully_contacts" formatted="false">Kişiler değiştirildi (%s)</string>
<string name="sync_successfully_calendar" formatted="false">Takvim \"%s\" değiştirildi (%s)</string>
<string name="sync_error_user_inactive">Kullanıcı etkin değil</string>
<string name="sync_error_unauthorized">Kimlik doğrulama başarısız oldu</string>
<string name="sync_phase_post_processing">son işlemler uygulanıyor</string>
<string name="sync_phase_push_entries">girdiler gönderiliyor</string>
<string name="sync_phase_apply_local_entries">yerel girdiler yeniden uygulanıyor</string>
<string name="sync_phase_apply_remote_entries">uzak girdiler uygulanıyor</string>
<string name="sync_phase_fetch_entries">uzak girdiler getiriliyor</string>
<string name="sync_phase_create_local_entries">yerel girdiler oluşturuluyor</string>
<string name="sync_phase_prepare_local">yerel girdiler hazırlanıyor</string>
<string name="sync_phase_prepare_fetch">getirmek için hazırlanıyor</string>
<string name="sync_phase_journals">günlükler eşzamanlanıyor</string>
<string name="sync_phase_prepare">eşzamanlama hazırlanıyor</string>
<string name="sync_error_journal_readonly">Günlük salt okunur</string>
<string name="sync_error_unavailable">%s sırasında sunucuya bağlanılamadı</string>
<string name="sync_error_integrity">%s sırasında bütünlük hatası</string>
<string name="sync_error_tasks">Görevleri eşzamanlama başarısız oldu (%s)</string>
<string name="sync_error_contacts">Kişileri eşzamanlama başarısız oldu (%s)</string>
<string name="sync_error_permissions_text">Ek izinler gerekli</string>
<string name="sync_error_permissions">EteSync izinleri</string>
<string name="debug_info_more_data_shared">Paylaş düğmesine tıklamak, aşağıdaki verilerin yanı sıra bazı ek hata ayıklama bilgilerinin ekli olduğu e-posta uygulamasını açacaktır. Bazı hassas bilgiler içerebilir, bu nedenle lütfen göndermeden önce inceleyin.</string>
<string name="journal_entries_loading">Değişiklik günlüğü girdileri yükleniyor...</string>
<string name="journal_entries_list_empty">Koleksiyon boş.
\nBelki hala eşzamanlanıyordur\?</string>
<string name="view_collection_members">Üyeleri Yönet</string>
<string name="view_collection_import">İçe aktar</string>
<string name="view_collection_edit">Düzenle</string>
<string name="create_collection_color">Koleksiyonun rengini ayarla</string>
<string name="edit_collection">Koleksiyonu düzenle</string>
<string name="install_opentasks">OpenTasks kur</string>
<string name="install_tasksorg">Tasks.org kur</string>
<string name="create_tasklist_display_name_hint">Görev Listem</string>
<string name="create_tasklist">Görev listesi oluştur</string>
<string name="create_calendar_display_name_hint">Takvimim</string>
<string name="create_calendar">Takvim oluştur</string>
<string name="settings_taskdav">Görevler</string>
<string name="settings_caldav">Takvim</string>
<string name="settings_carddav">Kişiler</string>
<string name="settings_sync_interval">Eşzamanlama aralığı</string>
<string name="settings_enter_encryption_password">Şifreleme parolanızı girin:</string>
<string name="settings_encryption_password_summary">Şifreleme parolanızı değiştirin</string>
<string name="settings_encryption_password">Şifreleme Parolası</string>
<string name="settings_password_summary">Farklı bir kimlik doğrulama parolası kullanın</string>
<string name="settings_encryption">Şifreleme</string>
<string name="settings_manage_account_summary">Ödeme bilgilerinizi, planınızı ve diğer hesap ayarlarınızı değiştirin</string>
<string name="settings_account_dashboard">Hesap Gösterge Tablosu</string>
<string name="settings_manage_account">Hesabı Yönet</string>
<string name="choose_file">Dosya seç</string>
<string name="import_account_top_notice">Lütfen tüm girdilerini içe aktaracağınız bir hesap seçin.</string>
<string name="import_permission_required">İçe aktarma için depolamayı okuma izni gereklidir.</string>
<string name="import_dialog_success">%1$d girdi işlendi.
\nEklendi: %2$d
\nDeğiştirildi: %3$d
\nAtlandı (başarısız oldu): %4$d</string>
<string name="import_dialog_adding_entries">Girdiler ekleniyor...</string>
<string name="import_dialog_loading_file">Dosya yükleniyor (biraz zaman alabilir)...</string>
<string name="import_dialog_failed_body">Nedeni: %s
\nGeliştiricilere bildirilsin mi\?</string>
<string name="import_dialog_failed_title">İçe Aktarma Başarısız</string>
<string name="import_dialog_title">İçe Aktar</string>
<string name="login_user_info_error_title">Kullanıcı Bilgilerini Ayarlama Hatası</string>
<string name="change_encryption_password_success_body">Şifreleme parolası başarıyla değiştirildi!</string>
<string name="change_encryption_password_success_title">Şifreleme Parolası Değişikliği</string>
<string name="change_encryption_password_are_you_sure">Devam etmek istediğinizden emin misiniz\? İşlem yarı yolda durdurulamaz veya geri alınamaz ve tamamlanması biraz zaman alabilir.</string>
<string name="change_encryption_password_new_password">Yeni şifreleme parolası</string>
<string name="change_encryption_password_extra_info">Şifreleme parolanızın ele geçirildiğine inanıyorsanız, lütfen bu aracı kullanmayın. Bunun yerine desteğe başvurun.</string>
<string name="change_encryption_password_title">Şifreleme Parolasını Değiştir</string>
<string name="wrong_encryption_password_content">Hesabınıza erişirken bir bütünlük hatası alındı, bu büyük olasılıkla yanlış şifreleme parolası girdiğiniz anlamına gelir.
\nLütfen kullanıcı adının büyük/küçük harfe duyarlı olduğunu unutmayın, bu nedenle lütfen farklı durumları deneyin, örneğin ilk karakteri büyük harf yapın.
\n
\nHata: %s</string>
<string name="wrong_encryption_password">Yanlış şifreleme parolası</string>
<string name="account_creation_failed">Hesap oluşturma başarısız oldu</string>
<string name="setting_up_encryption_content">Lütfen bekleyin, şifreleme ayarlanıyor…</string>
<string name="setting_up_encryption">Şifrelemeyi ayarlama</string>
<string name="login_forgot_password">Parolanızı mı unuttunuz\?</string>
<string name="login_service_details_description">Bu sizin oturum açma parolanızdır, şifreleme parolanız *değil*!</string>
<string name="login_encryption_account_label">Hesap:</string>
<string name="login_enter_encryption_details">Gizli Şifreleme Parolası</string>
<string name="login_enter_service_details">Oturum Açma Bilgilerini Girin</string>
<string name="login_finish">Bitir</string>
<string name="login_login">Oturum Aç</string>
<string name="login_encryption_extra_info">* Bu parola, hizmet üzerinde oturum açmak için kullanılan öncekinden farklı olarak verilerinizi şifrelemek için kullanılır.
\nGüvenlik nedeniyle ayrı bir şifreleme parolası seçmeniz istenir. Daha fazla bilgi için lütfen SSS bölümüne bakın: %s</string>
<string name="login_encryption_check_password">* Kaybolursa kurtarılamayacağı için lütfen parolanızı hatırladığınızdan emin olun!</string>
<string name="login_encryption_enter_password">\"%s\" olarak oturum açtınız. Devam etmek için lütfen şifreleme parolanızı girin veya yan menüden oturumu kapatın.</string>
<string name="login_encryption_set_new_password">Lütfen aşağıda şifreleme parolanızı belirleyin ve doğru parolayı girdiğinizden emin olun, çünkü kaybolursa kurtarılamaz!</string>
<string name="login_encryption_password">Şifreleme Parolası</string>
<string name="login_toggle_advanced">Gelişmiş ayarları göster</string>
<string name="login_custom_server_error">Geçersiz URL bulundu, https:// eklemeyi mi unuttunuz\?</string>
<string name="login_custom_server">EteSync Sunucu URL\'si</string>
<string name="permissions_tasks_org_request">Tasks.org izinlerini iste</string>
<string name="permissions_tasks_org_details">Görevleri yerel görev listelerinizle eşzamanlamak için EteSync\'in Tasks.org\'a erişmesi gerekiyor.</string>
<string name="permissions_tasks_org">Tasks.org izinleri</string>
<string name="permissions_opentasks_details">Görevleri yerel görev listelerinizle eşzamanlamak için EteSync\'in OpenTasks\'a erişmesi gerekiyor.</string>
<string name="permissions_contacts_details">Adres defterlerini yerel kişilerinizle eşzamanlamak için EteSync\'in kişilerinize erişmesi gerekiyor.</string>
<string name="permissions_calendar_details">Takvim etkinliklerini yerel takvimlerinizle eşzamanlamak için EteSync\'in takvimlerinize erişmesi gerekiyor.</string>
<string name="permissions_title">EteSync izinleri</string>
<string name="journal_item_restore_dialog_body">Girdi önceki durumuna başarıyla geri yüklendi.</string>
<string name="journal_item_restore_action">Durumu geri yükle</string>
<string name="journal_item_member_count">Üye Sayısı</string>
<string name="journal_item_member">Üye</string>
<string name="journal_item_relation">İlişki</string>
<string name="journal_item_birthday">Doğum günü</string>
<string name="journal_item_anniversary">Yıl dönümü</string>
<string name="journal_item_website">Web sitesi</string>
<string name="journal_item_note">Not</string>
<string name="journal_item_address">Adres</string>
<string name="journal_item_nickname">Takma ad</string>
<string name="journal_item_impp">Anlık Mesajlaşma</string>
<string name="journal_item_job_description">İş Tanımı</string>
<string name="journal_item_organization">Organizasyon</string>
<string name="journal_item_email">E-posta</string>
<string name="journal_item_phone">Telefon</string>
<string name="journal_item_reminders">Hatırlatmalar</string>
<string name="journal_item_attendees">Katılımcılar</string>
<string name="journal_item_tab_raw">Ham</string>
<string name="journal_item_tab_main">Ana</string>
<string name="about">Hakkında</string>
<string name="collection_members_remove">%s\'in erişimini iptal etmek ister misiniz\?
\nKötü niyetli bir kullanıcının, muhtemelen şifreleme anahtarlarına erişimi koruyabileceğini lütfen unutmayın. Daha fazla bilgi için lütfen SSS bölümüne bakın.</string>
<string name="collection_members_remove_title">Üyeyi kaldır</string>
<string name="collection_members_remove_error">Üye kaldırılırken hata oluştu</string>
<string name="collection_members_removing">Üye kaldırma</string>
<string name="collection_members_adding">Üye ekleme</string>
<string name="collection_members_error_user_not_found">Kullanıcı (%s) bulunamadı</string>
<string name="trust_fingerprint_body">Şifrelemenin güvenli olduğundan emin olmak için %s\'in güvenlik parmak izini doğrulayın.</string>
<string name="trust_fingerprint_title">Güvenlik parmak izini doğrula</string>
<string name="collection_members_add_error">Üye eklenirken hata oluştu</string>
<string name="collection_members_add">Üye ekle</string>
<string name="collection_members_list_empty">Üye yok</string>
<string name="collection_members_list_loading">Üyeler yükleniyor...</string>
<string name="collection_members_title">Üyeler</string>
<string name="use_native_apps_body">EteSync, Android ile sorunsuz bir şekilde bütünleşir, bu nedenle onu kullanmak için mevcut adres defteri ve takvim uygulamalarınızı kullanmanız yeterlidir!
\n
\nDaha fazla bilgi için lütfen kullanıcı rehberine bakın.</string>
<string name="use_native_apps_title">Biliyor musunuz\?</string>
<string name="members_old_journals_not_allowed">Eski tarz günlüklerin paylaşılmasına izin verilmiyor. Bu günlüğü paylaşmak için yeni bir tane oluşturun ve \"içe aktar\" iletişim kutusunu kullanarak içeriğini kopyalayın. Herhangi bir sorun yaşıyorsanız, lütfen desteğe başvurun.</string>
<string name="edit_owner_only">Sadece bu koleksiyonun sahibinin (%s) onu düzenlemesine izin verilir.</string>
<string name="not_allowed_title">İzin Yok</string>
<string name="members_owner_only">Sadece bu koleksiyonun sahibi (%s) üyelerini görüntüleyebilir.</string>
<string name="account_owner">Sahibi: %s</string>
<string name="account_showcase_import">Kişileri ve takvimleri EteSync\'e aktarmak için menüye tıklamanız ve \"İçe aktar\"ı seçmeniz gerekmektedir.</string>
<string name="tasks_not_showing">* Günlük kaydı sadece bir görev sağlayıcı kuruluysa gösterilir.</string>
<string name="change_journal_title">Günlüğü Değiştir</string>
<string name="show_fingperprint_title">Parmak İzim</string>
<string name="account_tasks_not_showing">* Görevler eşzamanlanmıyor (bir sağlayıcı kurun)</string>
<string name="account_click_install_tasks">OpenTasks kurmak için buraya tıklayın!</string>
<string name="account_showcase_view_collection">Koleksiyonu görüntülemek için bir ögeye tıklayabilirsiniz. Oradan günlüğü görüntüleyebilir, içe aktarabilir ve çok daha fazlasını yapabilirsiniz...</string>
<string name="account_delete_collection_last_text">Son koleksiyonun silinmesine izin verilmiyor, lütfen bunu silmek istiyorsanız yeni bir tane oluşturun.</string>
<string name="account_delete_collection_last_title">Son koleksiyon silinemiyor</string>
<string name="account_show_fingerprint">Parmak İzini Göster</string>
<string name="app_settings_change_notification">Değişiklik bildirimlerini göster</string>
<string name="app_settings_force_language_default">Otomatik</string>
<string name="app_settings_force_language">Dili Zorla</string>
<string name="app_settings_log_verbose_summary">Ayrıntılı günlük kayıtları özel bilgiler içerebilir</string>
<string name="app_settings_log_verbose">Ayrıntılı günlük kaydı</string>
<string name="app_settings_reset_certificates_success">Tüm özel sertifikalar temizlendi</string>
<string name="app_settings_reset_certificates_summary">Tüm özel sertifikaların güvenilirliğini sıfırlar</string>
<string name="app_settings_reset_certificates">Güvenil(mey)en sertifikaları sıfırla</string>
<string name="app_settings_distrust_system_certs_off">Sistem ve kullanıcı tarafından eklenen CA\'lara güvenilecek (tavsiye edilen)</string>
<string name="app_settings_distrust_system_certs_on">Sistem ve kullanıcı tarafından eklenen CA\'lara güvenilmeyecek</string>
<string name="app_settings_distrust_system_certs">Sistem sertifikalarına güvenme</string>
<string name="app_settings_override_proxy_port">HTTP vekil bağlantı noktası</string>
<string name="app_settings_override_proxy_host">HTTP vekil sunucu adı</string>
<string name="app_settings_override_proxy_off">Öntanımlı sistem vekil ayarlarını kullan</string>
<string name="app_settings_override_proxy_on">Özelleştirilmiş vekil ayarlarını kullan</string>
<string name="app_settings_override_proxy">Vekil ayarlarını geçersiz kıl</string>
<string name="app_settings_connection">Bağlantı</string>
<string name="app_settings_prefer_tasksorg_snack">Bu değişikliklerin geçerli olması için hesabınızı kaldırmanız ve yeniden eklemeniz gerekebilir.</string>
<string name="app_settings_prefer_tasksorg_off">Varsa görevleri OpenTasks ile eşzamanlayacak</string>
<string name="app_settings_prefer_tasksorg_on">Varsa görevleri Tasks.org ile eşzamanlayacak</string>
<string name="app_settings_prefer_tasksorg">Tasks.org görev sağlayıcısını tercih et</string>
<string name="app_settings_sync">Eşzamanla</string>
<string name="app_settings_notification_settings_summary">Bildirim kanallarını ve ayarlarını yönetin</string>
<string name="app_settings_notification_settings">Bildirim ayarları</string>
<string name="accounts_missing_permissions">Eksik izinler: %s</string>
<string name="accounts_showcase_add">EteSync\'i kullanmak için bir hesap eklemeniz gerekmektedir. Eklemek için buraya tıklayın...</string>
<string name="accounts_global_sync_enable">Etkinleştir</string>
<string name="accounts_global_sync_disabled">Sistem genelinde otomatik eşzamanlama devre dışı bırakıldı</string>
<string name="account_list_empty">EteSync\'e hoş geldiniz!</string>
<string name="navigation_drawer_contact">İletişim</string>
<string name="navigation_drawer_report_issue">Sorun bildir</string>
<string name="navigation_drawer_guide">Kullanıcı Rehberi</string>
<string name="navigation_drawer_subtitle">Secure Sync Adapter</string>
<string name="logging_davdroid_file_logging">EteSync dosya günlük kaydı AÇIK</string>
<string name="startup_vendor_specific_bugs_open_faq">SSS\'yi aç</string>
<string name="startup_vendor_specific_bugs_message">EteSync, sağlayıcıya özel hatalar içerebilecek bir Android sürümü kullandığınızı tespit etti.
\nDaha fazla bilgi için lütfen SSS bölümüne bakın.</string>
<string name="startup_vendor_specific_bugs">Muhtemel Sağlayıcı Hataları</string>
<string name="startup_development_version_give_feedback">Geri bildirimde bulun</string>
<string name="startup_development_version_message">Bu, EteSync\'in bir geliştirme sürümüdür. İşlerin beklendiği gibi çalışmayabileceğini unutmayın. EteSync\'i iyileştirmek için lütfen bize yapıcı geri bildirimde bulunun.</string>
<string name="startup_development_version">EteSync Ön İzleme Sürümü</string>
<string name="startup_battery_optimization_disable">EteSync için kapat</string>
<string name="startup_battery_optimization_message">Android, birkaç gün sonra EteSync eşzamanlamasını devre dışı bırakabilir/azaltabilir. Bunu önlemek için pil iyileştirmesini kapatın.</string>
<string name="startup_battery_optimization">Pil İyileştirmesi</string>
<string name="tourguide_title">Biliyor musunuz\?</string>
<string name="crash_email_body">Mümkünse, lütfen bu hatayı tetiklemek için yaptıklarınız gibi diğer ilişkili bilgileri ekleyin.</string>
<string name="crash_email_subject">EteSync Hata Ayıklama Bilgileri</string>
<string name="crash_message">Lütfen yığın izlemesini geliştiricilere gönderin.</string>
<string name="crash_title">EteSync çöktü!</string>
<string name="notification_channel_sync_status_desc">Eşzamanlama değişiklik özeti gibi bilgilendirici durum mesajları</string>
<string name="notification_channel_sync_status">Durum mesajları</string>
<string name="notification_channel_sync_warnings_desc">Salt-okunur koleksiyonların reddedilen değişiklikleri gibi önemli olmayan eşzamanlama sorunları</string>
<string name="notification_channel_sync_warnings">Eşzamanlama uyarıları</string>
<string name="sync_error_permission_denied">İzin reddedildi: %s</string>
<string name="sync_error_generic">Eşzamanlama başarısız oldu (%s)</string>
<string name="signup_password_restrictions">Parola en az 8 karakter uzunluğunda olmalıdır</string>
<string name="signup_title">Kayıt Ayrıntılarını Girin</string>
<string name="login_username_error">Geçerli bir kullanıcı adı gerekli</string>
<string name="login_username">Kullanıcı adı</string>
<string name="journal_item_tab_revisions">Düzeltmeler</string>
<string name="invitations_reject">Reddet</string>
<string name="invitations_accept">Kabul et</string>
<string name="invitations_accept_reject_dialog">Daveti kabul etmek mi yoksa reddetmek mi istiyorsunuz\?</string>
<string name="invitations_list_empty">Davet yok</string>
<string name="invitations_loading">Davetler yükleniyor...</string>
<string name="invitations_title">Davetler</string>
<string name="collection_members_leave">Ayrıl</string>
<string name="collection_members_no_access">Sadece yöneticilerin koleksiyon üyeliklerini yönetmesine izin verilir. Koleksiyondan ayrılmak ister misiniz\?</string>
<string name="collection_members_remove_admin">Yöneticilere erişimin kaldırılması şu anda desteklenmiyor.</string>
<string name="edit_owner_only_anon">Sadece bu koleksiyonun sahibinin onu düzenlemesine izin verilir.</string>
</resources>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="sync_successfully">
<item quantity="one">%d entry</item>
<item quantity="other">%d entries</item>
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>

@ -147,6 +147,7 @@
<string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string>
<string name="not_allowed_title">Not Allowed</string>
<string name="edit_owner_only">Only the owner of this collection (%s) is allowed to edit it.</string>
<string name="edit_owner_only_anon">Only the owner of this collection is allowed to edit it.</string>
<string name="members_old_journals_not_allowed">Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support.</string>
<string name="use_native_apps_title">Did you know?</string>
<string name="use_native_apps_body">EteSync integrates with your existing address book and calendar apps.\n\nPlease check out the user guide for more info.</string>
@ -165,10 +166,22 @@
<string name="collection_members_remove_error">Could not remove member</string>
<string name="collection_members_remove_title">Remove member</string>
<string name="collection_members_remove">Would you like to revoke %s\'s access?\nA malicious user could retain access to their encryption keys. Please refer to the FAQ for more info.</string>
<string name="collection_members_remove_admin">Removing access to admins is currently not supported.</string>
<string name="collection_members_no_access">Only admins are allowed to manage collection memberships. Would you like to leave the collection?</string>
<string name="collection_members_leave">Leave</string>
<!-- Invitations -->
<string name="invitations_title">Invitations</string>
<string name="invitations_loading">Loading invitations...</string>
<string name="invitations_list_empty">No invitations</string>
<string name="invitations_accept_reject_dialog">Would you like to accept or reject the invitation?</string>
<string name="invitations_accept">Accept</string>
<string name="invitations_reject">Reject</string>
<!-- JournalItemActivity -->
<string name="about">About</string>
<string name="journal_item_tab_main">Main</string>
<string name="journal_item_tab_revisions">Revisions</string>
<string name="journal_item_tab_raw">Raw</string>
<string name="journal_item_attendees">Attendees</string>
<string name="journal_item_reminders">Reminders</string>
@ -207,6 +220,8 @@
<!-- AddAccountActivity -->
<string name="login_title">Add account</string>
<string name="login_username">Username</string>
<string name="login_username_error">Valid username required</string>
<string name="login_email_address">E-mail</string>
<string name="login_email_address_error">Enter a valid e-mail address</string>
<string name="login_password">Password</string>
@ -216,8 +231,8 @@
<string name="login_encryption_password">Encryption Password</string>
<string name="login_encryption_set_new_password">Please set your encryption password below, and make sure you got it right, as it *can\'t* be recovered if lost!</string>
<string name="login_encryption_enter_password">You are logged in as \"%s\". Please enter your encryption password to continue, or log out from the side menu.</string>
<string name="login_encryption_check_password">* Please double-check the password, as it can\'t be recovered if wrong!</string>
<string name="login_encryption_extra_info">* This password is used to encrypt your data, unlike the previous one, which is used to log into the service.\nYou are asked to choose a separate encryption password for security reasons. For more info, please refer to the FAQ at: %s</string>
<string name="login_encryption_check_password">* Please double-check the password, as it can\'t be recovered if lost!</string>
<string name="login_encryption_extra_info">* This password is used to encrypt your data, unlike the previous one, which is used to log in.\nYou are asked to choose a separate encryption password for security reasons. For more info, please refer to the FAQ at: %s</string>
<string name="login_password_required">Password required</string>
<string name="login_login">Log In</string>
<string name="login_signup">Sign Up</string>
@ -234,13 +249,16 @@
<string name="login_wrong_username_or_password">Could not log in (probably wrong username or password).\nHave you registered?</string>
<string name="login_view_logs">View logs</string>
<string name="login_encryption_setup_title">Setting up encryption</string>
<string name="login_encryption_setup">Please wait, setting up encryption…</string>
<string name="setting_up_encryption">Setting up encryption</string>
<string name="setting_up_encryption_content">Please wait, setting up encryption…</string>
<string name="account_creation_failed">Could not create account</string>
<string name="wrong_encryption_password">Wrong encryption password</string>
<string name="wrong_encryption_password_content">Could not access your account. Most likely you entered the wrong encryption password.\nPlease note that the username is case sensitive, so please also try different capitalizations, for example make the first character uppercase.\n\nError: %s</string>
<string name="signup_title">Enter Signup Details</string>
<string name="signup_password_restrictions">Password should be at least 8 characters long</string>
<!-- ChangeEncryptionPasswordActivity -->
<string name="change_encryption_password_title">Change Encryption Password</string>
<string name="change_encryption_password_extra_info">Please don\'t use this tool if you believe your encryption password is in the wrong hands. Contact support instead.</string>
@ -332,7 +350,7 @@
<string name="delete_collection_deleting_collection">Deleting collection</string>
<!-- JournalViewer -->
<string name="journal_entries_list_empty">Journal is empty.\n(Maybe it\'s still syncing?)</string>
<string name="journal_entries_list_empty">The collection is empty.\n(Maybe it\'s still syncing?)</string>
<string name="journal_entries_loading">Loading journal entries…</string>
<!-- ExceptionInfoFragment -->
@ -347,6 +365,7 @@
<string name="debug_info_more_data_shared">Clicking \"Share\" will open the e-mail app with the data below, as well as some additional debug info, attached. It may contain some sensitive info, so please review it before sending.</string>
<string name="sync_error_permissions">EteSync permissions</string>
<string name="sync_error_permissions_text">Additional permissions required</string>
<string name="sync_error_generic">Sync failed (%s)</string>
<string name="sync_error_calendar">Could not sync calendar (%s)</string>
<string name="sync_error_contacts">Could not sync contacts (%s)</string>
<string name="sync_error_tasks">Fould not sync tasks (%s)</string>
@ -356,6 +375,7 @@
<string name="sync_error_unavailable">Could not connect to server while %s</string>
<string name="sync_error_local_storage">Database error while %s</string>
<string name="sync_error_journal_readonly">The journal is read-only</string>
<string name="sync_error_permission_denied">Permission denied: %s</string>
<string name="sync_phase_prepare">preparing syncing</string>
<string name="sync_phase_journals">syncronizing journals</string>
<string name="sync_phase_prepare_fetch">preparing for fetch</string>
@ -376,7 +396,6 @@
<string name="sync_journal_readonly">Journal \"%s\" is read-only</string>
<string name="sync_journal_readonly_message">The journal is read-only, so all of your changes (%d) have been reverted.</string>
<!-- Calendar invites -->
<string name="sync_calendar_attendees_notification_title" formatted="false">%s</string>
<string name="sync_calendar_attendees_notification_content">Send invitations to guests?</string>

@ -18,25 +18,12 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_encryption">
<PreferenceCategory android:title="@string/settings_sync">
<Preference
android:key="password"
android:title="@string/settings_encryption_password"
android:key="encryption_password"
android:summary="@string/settings_encryption_password_summary"
android:persistent="false"
/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_sync">
<EditTextPreference
android:key="password"
android:title="@string/settings_password"
android:persistent="false"
android:inputType="textPassword"
android:summary="@string/settings_password_summary"
android:dialogTitle="@string/settings_enter_password" />
/>
<ListPreference
android:key="sync_interval"

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2013 2015 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
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/settings_manage_account">
<Preference
android:key="manage_account"
android:title="@string/settings_account_dashboard"
android:persistent="false"
android:summary="@string/settings_manage_account_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_encryption">
<Preference
android:title="@string/settings_encryption_password"
android:key="encryption_password"
android:summary="@string/settings_encryption_password_summary"
android:persistent="false"
/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_sync">
<EditTextPreference
android:key="password"
android:title="@string/settings_password"
android:persistent="false"
android:inputType="textPassword"
android:summary="@string/settings_password_summary"
android:dialogTitle="@string/settings_enter_password" />
<ListPreference
android:key="sync_interval"
android:persistent="false"
android:title="@string/settings_sync_interval"
android:entries="@array/settings_sync_interval_names"
android:entryValues="@array/settings_sync_interval_seconds" />
<SwitchPreferenceCompat
android:key="sync_wifi_only"
android:persistent="false"
android:title="@string/settings_sync_wifi_only"
android:summaryOn="@string/settings_sync_wifi_only_on"
android:summaryOff="@string/settings_sync_wifi_only_off"
/>
<EditTextPreference
android:key="sync_wifi_only_ssid"
android:dependency="sync_wifi_only"
android:persistent="false"
android:title="@string/settings_sync_wifi_only_ssid"
android:dialogMessage="@string/settings_sync_wifi_only_ssid_message"/>
</PreferenceCategory>
</PreferenceScreen>
Loading…
Cancel
Save