1
0
mirror of https://github.com/etesync/android synced 2024-11-22 07:58:09 +00:00

Merge branch 'master' into patch-1

This commit is contained in:
Allan Nordhøy 2020-12-27 16:31:43 +00:00 committed by GitHub
commit a3d6d4f3d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 2024 additions and 456 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: etesync
custom: https://www.etesync.com/contribute/#donate

View File

@ -1,6 +1,50 @@
# Changelog
*NOTE:* may be removed in the future in favor of the fastlane changelog.
## Version 2.2.3
* Fix issues with the Tasks.org integration and subtasks (due to rewriting UIDs).
## Version 2.2.2
* Fix "potential vendor bugs" message constantly showing.
## Version 2.2.1
* Fix crash when importing events and also when syncing legacy events
## Version 2.2.0
* Support resizable activities
* Update ical4android dep - should fix issues with duplicate tasks and events
* Update vcard4android dep
* Update gradle and sdk version
* Update translations
## Version 2.1.5
* Improve error handling in sync and import
* Update translations
* Fix some crashes
## Version 2.1.4
* Event invitations: only send invitations if we are the organizers
* Fix rare crash when pushing changes with EteSync 1.0 accounts
## Version 2.1.3
* Fix crashes on older Android devices
* Fix crashes with some screen not loading for some users.
## Version 2.1.2
* Fix crash when generating email invitations while using a French locale
* Uptdate etebase dep to fix issue with custom urls not ending with a slash.
## Version 2.1.1
* Debug info: fix manually sending of crash reports to have visual feedback.
* Debug info: fix manually sending of crash reports to include more crash information.
* Fixed a few crashes that were happening in some rare cases.
## Version 2.1.0
* Change the crash reporting to not rely on email (use HTTP instead)
## Version 2.0.0
* EteSync 2.0 support \o/
## Version 1.16.2
* Update OkHttp3 dependency.

View File

@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode 200
versionName "2.0.0"
versionCode 20203
versionName "2.2.3"
buildConfigField "boolean", "customCerts", "true"
}
@ -122,6 +122,9 @@ android {
buildTypes.release.signingConfig = null
}
compileOptions {
// enable because ical4android requires desugaring
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@ -130,20 +133,24 @@ android {
jvmTarget = "1.8"
}
dataBinding.enabled = true
buildFeatures {
dataBinding = true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
implementation "org.jetbrains.anko:anko-commons:0.10.4"
implementation "com.etesync:journalmanager:1.1.1"
def etebaseVersion = '0.2.2'
def etebaseVersion = '2.3.2'
implementation "com.etebase:client:$etebaseVersion"
def acraVersion = '5.3.0'
implementation "ch.acra:acra-mail:$acraVersion"
implementation "ch.acra:acra-notification:$acraVersion"
def acraVersion = '5.7.0'
implementation "ch.acra:acra-http:$acraVersion"
implementation "ch.acra:acra-dialog:$acraVersion"
def supportVersion = '1.0.0'
implementation "androidx.legacy:legacy-support-core-ui:$supportVersion"
implementation "androidx.core:core:$supportVersion"

View File

@ -10,7 +10,6 @@
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# Kotlin
-dontwarn kotlin.**
@ -25,14 +24,7 @@
-keep class ezvcard.property.** { *; } # keep all vCard properties (created at runtime)
# ical4j: ignore unused dynamic libraries
-dontwarn aQute.**
-dontwarn groovy.** # Groovy-based ContentBuilder not used
-dontwarn javax.cache.** # no JCache support in Android
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class org.threeten.bp.** { *; } # keep ThreeTen (for time zone processing)
# okhttp
# JSR 305 annotations are for embedding nullability information.

View File

@ -52,6 +52,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:resizeableActivity="true"
tools:ignore="UnusedAttribute">
<receiver
@ -245,6 +246,10 @@
android:name=".ui.etebase.InvitationsActivity"
android:exported="false"
/>
<activity
android:name=".ui.MigrateV2Activity"
android:exported="false"
/>
<activity
android:name=".ui.ViewCollectionActivity"
android:exported="false"

View File

@ -3,11 +3,10 @@ package com.etesync.syncadapter;
import android.content.Context;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.config.NotificationConfigurationBuilder;
import org.acra.config.DialogConfigurationBuilder;
import org.acra.config.HttpSenderConfigurationBuilder;
import org.acra.data.StringFormat;
import static com.etesync.syncadapter.utils.EventEmailInvitationKt.emailSupportsAttachments;
import org.acra.sender.HttpSender;
public class AcraConfiguration {
public static CoreConfigurationBuilder getConfig(Context context) {
@ -15,17 +14,14 @@ public class AcraConfiguration {
.setBuildConfigClass(BuildConfig.class)
.setLogcatArguments("-t", "500", "-v", "time")
.setReportFormat(StringFormat.JSON);
builder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class)
.setMailTo("reports@etesync.com")
.setResSubject(R.string.crash_email_subject)
.setReportFileName("ACRA-report.stacktrace.json")
.setReportAsFile(emailSupportsAttachments(context))
builder.getPluginConfigurationBuilder(HttpSenderConfigurationBuilder.class)
.setUri(Constants.crashReportingUrl)
.setHttpMethod(HttpSender.Method.POST)
.setEnabled(true);
builder.getPluginConfigurationBuilder(NotificationConfigurationBuilder.class)
builder.getPluginConfigurationBuilder(DialogConfigurationBuilder.class)
.setResTitle(R.string.crash_title)
.setResText(R.string.crash_message)
.setResChannelName(R.string.notification_channel_crash_reports)
.setSendOnClick(true)
.setResCommentPrompt(R.string.crash_email_body)
.setEnabled(true);
return builder;

View File

@ -25,14 +25,17 @@ public class Constants {
NOTIFICATION_PERMISSIONS = 20;
public static final Uri webUri = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://www.etesync.com/" : DEBUG_REMOTE_URL);
public static final Uri etebaseDashboardPrefix = Uri.parse("https://dashboard.etebase.com/user/partner/");
public static final Uri contactUri = webUri.buildUpon().appendEncodedPath("about/#contact").build();
public static final Uri registrationUrl = webUri.buildUpon().appendEncodedPath("accounts/signup/").build();
public static final Uri reportIssueUri = Uri.parse("https://github.com/etesync/android/issues");
public static final Uri feedbackUri = reportIssueUri;
public static final Uri pricing = webUri.buildUpon().appendEncodedPath("pricing/").build();
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 = faqUri.buildUpon().fragment("forgot-password").build();
public static final String crashReportingUrl = "https://www.etesync.com/crash/android-syncadapter/report/";
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;
@ -48,4 +51,9 @@ public class Constants {
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";
public final static String[] COLLECTION_TYPES = new String[] {
ETEBASE_TYPE_ADDRESS_BOOK,
ETEBASE_TYPE_CALENDAR,
ETEBASE_TYPE_TASKS
};
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import com.etebase.client.*
import com.etebase.client.Collection
import com.etebase.client.exceptions.EtebaseException
import com.etebase.client.exceptions.UrlParseException
import okhttp3.OkHttpClient
import java.io.File
import java.util.*
@ -43,13 +44,13 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
return fsCache._unstable_collectionList(colMgr).filter {
withDeleted || !it.isDeleted
}.map{
CachedCollection(it, it.meta)
CachedCollection(it, it.meta, it.collectionType)
}
}
fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection {
return fsCache.collectionGet(colMgr, colUid).let {
CachedCollection(it, it.meta)
CachedCollection(it, it.meta, it.collectionType)
}
}
@ -58,7 +59,11 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
}
fun collectionUnset(colMgr: CollectionManager, colUid: String) {
fsCache.collectionUnset(colMgr, colUid)
try {
fsCache.collectionUnset(colMgr, colUid)
} catch (e: UrlParseException) {
// Ignore, as it just means the file doesn't exist
}
}
fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List<CachedItem> {
@ -119,6 +124,6 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
}
}
data class CachedCollection(val col: Collection, val meta: CollectionMetadata)
data class CachedCollection(val col: Collection, val meta: ItemMetadata, val collectionType: String)
data class CachedItem(val item: Item, val meta: ItemMetadata, val content: String)

View File

@ -424,7 +424,7 @@ class LocalAddressBook(
val values = ContentValues(1)
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
return ContentUris.parseId(uri)
return ContentUris.parseId(uri!!)
}
fun removeEmptyGroups() {

View File

@ -10,7 +10,6 @@ package com.etesync.syncadapter.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
@ -24,7 +23,6 @@ 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
@ -210,15 +208,15 @@ class LocalCalendar private constructor(
cursor2!!.close()
val batch = BatchOperation(provider)
// re-schedule original event and set it to DIRTY
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
batch.enqueue(
BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
.withValue(Events.DIRTY, 1)
))
)
// remove exception
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
))
batch.enqueue(
BatchOperation.CpoBuilder.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
)
batch.commit()
}
cursor!!.close()
@ -242,16 +240,14 @@ class LocalCalendar private constructor(
val batch = BatchOperation(provider)
// original event to DIRTY
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, 1)
))
)
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
))
)
batch.commit()
}
cursor!!.close()

View File

@ -141,17 +141,15 @@ class LocalContact : AndroidContact, LocalAddress {
super.insertDataRows(batch)
if (contact?.unknownProperties != null) {
val op: BatchOperation.Operation
val builder = ContentProviderOperation.newInsert(dataSyncURI())
var builder = BatchOperation.CpoBuilder.newInsert(dataSyncURI())
if (id == null) {
op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
builder = builder.withValue(UnknownProperties.RAW_CONTACT_ID, 0)
} else {
op = BatchOperation.Operation(builder)
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
builder = builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
}
builder.withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact?.unknownProperties)
batch.enqueue(op)
batch.enqueue(builder)
}
}
@ -166,7 +164,7 @@ class LocalContact : AndroidContact, LocalAddress {
return this.add()
}
override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) {
override fun buildContact(builder: BatchOperation.CpoBuilder, update: Boolean) {
super.buildContact(builder, update)
builder.withValue(ContactsContract.RawContacts.DIRTY, if (saveAsDirty) 1 else 0)
}
@ -202,10 +200,10 @@ class LocalContact : AndroidContact, LocalAddress {
if (batch == null)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
else {
val builder = ContentProviderOperation
val builder = BatchOperation.CpoBuilder
.newUpdate(rawContactSyncURI())
.withValues(values)
batch.enqueue(BatchOperation.Operation(builder))
.withValue(COLUMN_HASHCODE, hashCode)
batch.enqueue(builder)
}
}
@ -223,33 +221,28 @@ class LocalContact : AndroidContact, LocalAddress {
fun addToGroup(batch: BatchOperation, groupID: Long) {
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI())
batch.enqueue(BatchOperation.CpoBuilder.newInsert(dataSyncURI())
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.RAW_CONTACT_ID, id)
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
))
)
groupMemberships.add(groupID)
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI())
batch.enqueue(BatchOperation.CpoBuilder.newInsert(dataSyncURI())
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
.withValue(CachedGroupMembership.GROUP_ID, groupID)
.withYieldAllowed(true)
))
)
cachedGroupMemberships.add(groupID)
}
fun removeGroupMemberships(batch: BatchOperation) {
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(dataSyncURI())
batch.enqueue(BatchOperation.CpoBuilder.newDelete(dataSyncURI())
.withSelection(
Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)",
arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
)
.withYieldAllowed(true)
))
)
groupMemberships.clear()
cachedGroupMemberships.clear()
}

View File

@ -83,7 +83,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
weAreOrganizer = isOrganizer != null && isOrganizer != 0
}
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
override fun buildEvent(recurrence: Event?, builder: BatchOperation.CpoBuilder) {
super.buildEvent(recurrence, builder)
val buildException = recurrence != null
@ -101,7 +101,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
.withValue(COLUMN_ETAG, eTag)
}
override fun insertReminder(batch: BatchOperation, idxEvent: Int, alarm: VAlarm) {
override fun insertReminder(batch: BatchOperation, idxEvent: Int?, alarm: VAlarm) {
// We only support DISPLAY and AUDIO alarms so modify when inserting
val action = alarm.action
val modifiedAlarm = when (action?.value) {
@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
override fun legacyPrepareForUpload(fileName_: String?) {
var uid: String? = null
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)!!
if (c.moveToNext())
uid = c.getString(0)
if (uid == null)

View File

@ -135,23 +135,20 @@ class LocalGroup : AndroidGroup, LocalAddress {
val batch = BatchOperation(addressBook.provider!!)
// delete cached group memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
batch.enqueue(BatchOperation.CpoBuilder.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString())
)
))
)
// insert updated cached group memberships
for (member in getMembers())
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
batch.enqueue(BatchOperation.CpoBuilder.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
.withValue(CachedGroupMembership.GROUP_ID, id)
.withYieldAllowed(true)
))
)
batch.commit()
}
@ -214,11 +211,9 @@ class LocalGroup : AndroidGroup, LocalAddress {
.forEach { it.updateHashCode(batch) }
// remove pending memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
))
)
}

View File

@ -12,10 +12,7 @@ import android.content.ContentProviderOperation
import android.content.ContentValues
import android.net.Uri
import android.text.TextUtils
import at.bitfire.ical4android.AndroidTask
import at.bitfire.ical4android.AndroidTaskFactory
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.*
import com.etesync.syncadapter.log.Logger
import org.dmfs.tasks.contract.TaskContract
import java.io.ByteArrayOutputStream
@ -75,7 +72,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
task?.sequence = values.getAsInteger(COLUMN_SEQUENCE)
}
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
override fun buildTask(builder: BatchOperation.CpoBuilder, update: Boolean) {
super.buildTask(builder, update)
builder.withValue(TaskContract.Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task?.uid)
@ -98,7 +95,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
override fun legacyPrepareForUpload(fileName_: String?) {
var uid: String? = null
val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null)
val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null)!!
if (c.moveToNext())
uid = c.getString(0)
if (uid == null)

View File

@ -77,7 +77,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
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 }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK }
}
for (collection in collections) {

View File

@ -147,7 +147,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
for (local in localDirty) {
val event = local.event
if (event?.attendees?.isEmpty()!!) {
if (event?.attendees?.isEmpty()!! || !event.organizer?.value?.replace("mailto:", "").equals(account.name)) {
return
}
createInviteAttendeesNotification(event, local.content)

View File

@ -63,7 +63,7 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
}
for (collection in collections) {

View File

@ -102,11 +102,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
val currentGroups = contact.getGroupMemberships()
for (groupID in SetUtils.disjunction(cachedGroups, currentGroups)) {
Logger.log.fine("Marking group as dirty: " + groupID!!)
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, groupID)))
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, groupID)))
.withValue(ContactsContract.Groups.DIRTY, 1)
.withYieldAllowed(true)
))
)
}
} catch (ignored: FileNotFoundException) {
}

View File

@ -26,10 +26,11 @@ 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.Constants.COLLECTION_TYPES
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
@ -243,7 +244,6 @@ abstract class SyncAdapterService : Service() {
return
}
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
synchronized(etebaseLocalCache) {
val cacheAge = 5 * 1000 // 5 seconds - it's just a hack for burst fetching
@ -259,7 +259,7 @@ abstract class SyncAdapterService : Service() {
var stoken = etebaseLocalCache.loadStoken()
var done = false
while (!done) {
val colList = colMgr.list(FetchOptions().stoken(stoken))
val colList = colMgr.list(COLLECTION_TYPES, FetchOptions().stoken(stoken))
for (col in colList.data) {
etebaseLocalCache.collectionSet(colMgr, col)
}

View File

@ -571,7 +571,7 @@ constructor(protected val context: Context, protected val account: Account, prot
break
}
Logger.log.info("Added/changed resource with UUID: " + local.uuid)
local.clearDirty(local.uuid!!)
local.clearDirty(local.uuid)
}
if (left > 0) {
localDirty = localDirty.drop(left)
@ -644,15 +644,48 @@ constructor(protected val context: Context, protected val account: Account, prot
item.meta = meta
}
private fun prepareLocalItemForUpload(colUid: String, local: T): Item {
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 = local.uuid ?: 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
}
return item
}
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)
val item = prepareLocalItemForUpload(colUid, local)
item.delete()
ret.add(item)
if (ret.size == MAX_PUSH) {
@ -663,34 +696,7 @@ constructor(protected val context: Context, protected val account: Account, prot
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
}
val item = prepareLocalItemForUpload(colUid, local)
ret.add(item)

View File

@ -129,6 +129,8 @@ class SyncNotification(internal val context: Context, internal val notificationT
val detailsIntent: Intent
if (e is Exceptions.UnauthorizedException) {
detailsIntent = Intent(this, AccountSettingsActivity::class.java)
} else if (e is PermissionDeniedException || e is UnauthorizedException) {
detailsIntent = Intent(this, AccountSettingsActivity::class.java)
} else if (e is Exceptions.UserInactiveException) {
WebViewActivity.openUrl(this, Constants.dashboard)
return

View File

@ -83,7 +83,7 @@ class TasksSyncAdapterService: SyncAdapterService() {
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_TASKS }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_TASKS }
}
for (collection in collections) {

View File

@ -100,7 +100,7 @@ class AboutActivity : BaseActivity() {
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Spanned> {
return LicenseLoader(context!!, args!!.getString(KEY_FILE_NAME))
return LicenseLoader(requireContext(), args!!.getString(KEY_FILE_NAME)!!)
}
override fun onLoadFinished(loader: Loader<Spanned>, license: Spanned) {

View File

@ -10,7 +10,6 @@ package com.etesync.syncadapter.ui
import android.accounts.Account
import android.accounts.AccountManager
import android.app.LoaderManager
import android.content.*
import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
import android.net.Uri
@ -22,9 +21,14 @@ import android.provider.ContactsContract
import android.text.TextUtils
import android.view.*
import android.widget.*
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import at.bitfire.ical4android.TaskProvider.Companion.TASK_PROVIDERS
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.CollectionAccessLevel
@ -52,11 +56,14 @@ import com.etesync.syncadapter.utils.ShowcaseBuilder
import com.etesync.syncadapter.utils.packageInstalled
import com.google.android.material.snackbar.Snackbar
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.acra.ACRA
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import tourguide.tourguide.ToolTip
import java.util.logging.Level
class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo>, Refreshable {
class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, Refreshable {
private val model: AccountInfoViewModel by viewModels()
private lateinit var account: Account
private lateinit var settings: AccountSettings
@ -101,10 +108,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.getParcelableExtra(EXTRA_ACCOUNT)
account = intent.getParcelableExtra(EXTRA_ACCOUNT)!!
title = account.name
settings = AccountSettings(this, account)
// Set it for ACRA in case we crash in any of the user views
ACRA.getErrorReporter().putCustomData("username", account.name)
setContentView(R.layout.activity_account)
val icMenu = ContextCompat.getDrawable(this, R.drawable.ic_menu_light)
@ -139,7 +149,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
// load CardDAV/CalDAV journals
loaderManager.initLoader(0, intent.extras, this)
if (savedInstanceState == null) {
model.initialize(this, account)
model.loadAccount()
model.observe(this) {
updateUi(it)
}
}
if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) {
ShowcaseBuilder.getBuilder(this)
@ -160,6 +176,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
if (settings.isLegacy) {
val invitations = menu.findItem(R.id.invitations)
invitations.setVisible(false)
menu.findItem(R.id.migration_v2).setVisible(true)
}
return true
}
@ -194,6 +211,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
val intent = InvitationsActivity.newIntent(this, account)
startActivity(intent)
}
R.id.migration_v2 -> {
val intent = MigrateV2Activity.newIntent(this, account)
startActivity(intent)
}
else -> return super.onOptionsItemSelected(item)
}
return true
@ -272,15 +293,11 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountInfo> {
return AccountLoader(this, account)
}
override fun refresh() {
loaderManager.restartLoader(0, intent.extras, this)
model.loadAccount()
}
override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo) {
fun updateUi(info: AccountInfo) {
accountInfo = info
if (info.carddav != null) {
@ -331,41 +348,47 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
}
override fun onLoaderReset(loader: Loader<AccountInfo>) {
if (listCardDAV != null)
listCardDAV!!.adapter = null
if (listCalDAV != null)
listCalDAV!!.adapter = null
if (listTaskDAV != null)
listTaskDAV!!.adapter = null
}
private class AccountLoader(context: Context, private val account: Account) : AsyncTaskLoader<AccountInfo>(context), AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver {
class AccountInfoViewModel : ViewModel(), AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver {
private val holder = MutableLiveData<AccountActivity.AccountInfo>()
private lateinit var context: Context
private lateinit var account: Account
private var davService: AccountUpdateService.InfoBinder? = null
private var syncStatusListener: Any? = null
override fun onStartLoading() {
fun initialize(context: Context, account: Account) {
this.context = context
this.account = account
syncStatusListener = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE, this)
context.bindService(Intent(context, AccountUpdateService::class.java), this, Context.BIND_AUTO_CREATE)
}
override fun onStopLoading() {
fun loadAccount() {
doAsync {
val info = doLoad()
uiThread {
holder.value = info
}
}
}
override fun onCleared() {
davService?.removeRefreshingStatusListener(this)
context.unbindService(this)
if (syncStatusListener != null)
if (syncStatusListener != null) {
ContentResolver.removeStatusChangeListener(syncStatusListener)
syncStatusListener = null
}
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
davService = service as AccountUpdateService.InfoBinder
davService!!.addRefreshingStatusListener(this, false)
forceLoad()
loadAccount()
}
override fun onServiceDisconnected(name: ComponentName) {
@ -373,18 +396,19 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
override fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean) {
forceLoad()
loadAccount()
}
override fun onStatusChanged(which: Int) {
forceLoad()
loadAccount()
}
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)
CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description
?: "", info.color, it.isReadOnly, isAdmin, info)
}
}
@ -398,8 +422,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
synchronized(etebaseLocalCache) {
return etebaseLocalCache.collectionList(colMgr).map {
val meta = it.meta
val collectionType = it.collectionType
if (strType != meta.collectionType) {
if (strType != collectionType) {
return@map null
}
@ -409,14 +434,14 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
val metaColor = meta.color
val color = if (!metaColor.isNullOrBlank()) LocalCalendar.parseColor(metaColor) else null
CollectionListItemInfo(it.col.uid, type, meta.name, meta.description
CollectionListItemInfo(it.col.uid, type, meta.name!!, meta.description
?: "", color, isReadOnly, isAdmin, null)
}.filterNotNull()
}
}
override fun loadInBackground(): AccountInfo {
val info = AccountInfo()
private fun doLoad(): AccountActivity.AccountInfo {
val info = AccountActivity.AccountInfo()
val settings: AccountSettings
try {
settings = AccountSettings(context, account)
@ -432,7 +457,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
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!!.refreshing = ContentResolver.isSyncActive(account, App.addressBooksAuthority)
info.carddav!!.infos = getLegacyJournals(data, serviceEntity)
val accountManager = AccountManager.get(context)
@ -448,16 +473,14 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
CollectionInfo.Type.CALENDAR -> {
info.caldav = AccountInfo.ServiceInfo()
info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
info.caldav!!.refreshing = 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) ||
TASK_PROVIDERS.any {
ContentResolver.isSyncActive(account, it.authority)
}
info.taskdav!!.refreshing = TASK_PROVIDERS.any {
ContentResolver.isSyncActive(account, it.authority)
}
info.taskdav!!.infos = getLegacyJournals(data, serviceEntity)
}
}
@ -497,6 +520,12 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
return info
}
fun observe(owner: LifecycleOwner, observer: (AccountActivity.AccountInfo) -> Unit) =
holder.observe(owner, observer)
val value: AccountActivity.AccountInfo?
get() = holder.value
}
@ -606,4 +635,4 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
private val HINT_VIEW_COLLECTION = "ViewCollection"
}
}
}

View File

@ -17,20 +17,24 @@ import android.os.Bundle
import android.provider.CalendarContract
import android.text.TextUtils
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NavUtils
import androidx.core.net.toUri
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.Companion.TASK_PROVIDERS
import com.etebase.client.exceptions.EtebaseException
import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.ui.setup.LoginCredentials
import com.etesync.syncadapter.ui.setup.LoginCredentialsChangeFragment
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
class AccountSettingsActivity : BaseActivity() {
private lateinit var account: Account
@ -38,7 +42,7 @@ class AccountSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.getParcelableExtra(KEY_ACCOUNT)
account = intent.getParcelableExtra(KEY_ACCOUNT)!!
title = getString(R.string.settings_title, account.name)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
@ -81,18 +85,40 @@ class AccountSettingsFragment() : PreferenceFragmentCompat(), LoaderManager.Load
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
return AccountSettingsLoader(requireContext(), (args!!.getParcelable(KEY_ACCOUNT) as Account?)!!)
}
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
if (settings == null) {
activity!!.finish()
activity?.finish()
return
}
// Category: dashboard
val prefManageAccount = findPreference("manage_account")
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
Toast.makeText(requireContext(), "Not yet supported", Toast.LENGTH_LONG).show()
doAsync {
try {
val httpClient = HttpClient.Builder(requireContext()).build()
val etebase = EtebaseLocalCache.getEtebase(requireContext(), httpClient.okHttpClient, settings)
val url = etebase.fetchDashboardUrl()
uiThread {
WebViewActivity.openUrl(requireActivity(), url.toUri())
}
} catch (e: EtebaseException) {
uiThread {
val context = context
if (context != null) {
AlertDialog.Builder(context)
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }
.show()
}
}
}
}
true
}
@ -171,7 +197,7 @@ class LegacyAccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
return AccountSettingsLoader(context!!, (args!!.getParcelable(KEY_ACCOUNT) as? Account)!!)
}
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {

View File

@ -68,7 +68,7 @@ class CollectionMembersActivity : BaseActivity(), Refreshable {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
info = intent.extras!!.getSerializable(EXTRA_COLLECTION_INFO) as CollectionInfo
refresh()

View File

@ -37,7 +37,7 @@ class CreateCollectionFragment : DialogFragment(), LoaderManager.LoaderCallbacks
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = arguments!!.getParcelable(ARG_ACCOUNT)
account = arguments!!.getParcelable(ARG_ACCOUNT)!!
info = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo
loaderManager.initLoader(0, null, this)

View File

@ -27,9 +27,9 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import at.bitfire.ical4android.TaskProvider.ProviderName
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.journalmanager.Exceptions.HttpException
import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.journalmanager.Exceptions.HttpException
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.EntryEntity
import com.etesync.syncadapter.model.JournalEntity
@ -66,7 +66,11 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
fun onShare(item: MenuItem) {
ACRA.getErrorReporter().putCustomData("debug_info", report)
ACRA.getErrorReporter().handleSilentException(null)
val account: Account? = intent.extras?.getParcelable(KEY_ACCOUNT)
if (account != null) {
ACRA.getErrorReporter().putCustomData("username", account.name)
}
ACRA.getErrorReporter().handleException(intent.extras?.getSerializable(KEY_THROWABLE) as Throwable?)
ACRA.getErrorReporter().removeCustomData("debug_info")
}

View File

@ -264,7 +264,13 @@ class JournalItemActivity : BaseActivity(), Refreshable {
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))
val dtStart = event.dtStart?.date?.time
val dtEnd = event.dtEnd?.date?.time
if ((dtStart == null) || (dtEnd == null)) {
setTextViewText(view, R.id.when_datetime, getString(R.string.loading_error_title))
} else {
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(dtStart, dtEnd, event.isAllDay(), context))
}
setTextViewText(view, R.id.where, event.location)

View File

@ -0,0 +1,821 @@
package com.etesync.syncadapter.ui
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Dialog
import android.app.ProgressDialog
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.ContactsContract
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.*
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.Task
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import com.etebase.client.Account as EtebaseAccount
import com.etebase.client.Client
import com.etebase.client.Item
import com.etebase.client.ItemMetadata
import com.etesync.journalmanager.model.SyncEntry
import com.etesync.syncadapter.*
import com.etesync.syncadapter.model.*
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.ui.etebase.*
import com.etesync.syncadapter.ui.setup.CreateAccountFragment
import com.etesync.syncadapter.ui.setup.LoginCredentials
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 okhttp3.Request
import okhttp3.RequestBody
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.StringReader
import java.net.URI
import java.util.*
import kotlin.collections.HashMap
class MigrateV2Activity : BaseActivity() {
private lateinit var accountV1: Account
private val model: AccountViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accountV1 = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
setContentView(R.layout.etebase_fragment_activity)
if (savedInstanceState == null) {
setTitle(R.string.migrate_v2_wizard_welcome_title)
supportFragmentManager.commit {
replace(R.id.fragment_container, WizardWelcomeFragment.newInstance(accountV1))
}
}
}
companion object {
private val EXTRA_ACCOUNT = "account"
fun newIntent(context: Context, account: Account): Intent {
val intent = Intent(context, MigrateV2Activity::class.java)
intent.putExtra(EXTRA_ACCOUNT, account)
return intent
}
}
}
fun reportErrorHelper(context: Context, e: Throwable) {
AlertDialog.Builder(context)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
class WizardWelcomeFragment : Fragment() {
internal lateinit var accountV1: Account
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.migrate_v2_wizard_welcome, container, false)
if (savedInstanceState == null) {
if (container != null) {
initUi(inflater, ret)
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, v: View) {
v.findViewById<Button>(R.id.signup).setOnClickListener {
parentFragmentManager.commit {
replace(R.id.fragment_container, SignupFragment.newInstance(accountV1))
addToBackStack(null)
}
}
v.findViewById<Button>(R.id.login).setOnClickListener {
parentFragmentManager.commit {
replace(R.id.fragment_container, LoginFragment.newInstance(accountV1))
addToBackStack(null)
}
}
}
companion object {
fun newInstance(accountV1: Account): WizardWelcomeFragment {
val ret = WizardWelcomeFragment()
ret.accountV1 = accountV1
return ret
}
}
}
class SignupFragment : 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
internal lateinit var accountV1: Account
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)
// Hide stuff we don't need for the migration tool
v.findViewById<View>(R.id.trial_notice).visibility = View.GONE
editEmail.visibility = View.GONE
editEmail.editText?.setText(accountV1.name)
val login = v.findViewById<Button>(R.id.login)
login.visibility = View.GONE
val createAccount = v.findViewById<Button>(R.id.create_account)
createAccount.setOnClickListener {
val credentials = validateData()
if (credentials != null) {
SignupDoFragment.newInstance(accountV1, credentials).show(requireFragmentManager(), 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()
// FIXME: this validation should only be done in the server, we are doing it here until the Java library supports field errors
if ((userName.length < 6) || (!userName.matches(Regex("""^[\w.-]+$""")))) {
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.length < 8) {
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
}
companion object {
fun newInstance(accountV1: Account): SignupFragment {
val ret = SignupFragment()
ret.accountV1 = accountV1
return ret
}
}
}
class SignupDoFragment : DialogFragment() {
private val model: ConfigurationViewModel by activityViewModels()
internal lateinit var accountV1: Account
internal lateinit var signupCredentials: SignupCredentials
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) {
val settings = AccountSettings(requireContext(), accountV1)
// Mark the etesync v1 account as wanting migration
doAsync {
val httpClient = HttpClient.Builder(context, settings).setForeground(true).build().okHttpClient
val remote = settings.uri!!.toHttpUrlOrNull()!!.newBuilder()
.addPathSegments("etesync-v2/confirm-migration/")
.build()
val body = RequestBody.create(null, byteArrayOf())
val request = Request.Builder()
.post(body)
.url(remote)
.build()
val response = httpClient.newCall(request).execute()
uiThread {
if (context == null) {
dismissAllowingStateLoss()
return@uiThread
}
if (response.isSuccessful) {
model.signup(requireContext(), signupCredentials)
} else {
if (response.code == 400) {
reportErrorHelper(requireContext(), Error("User already migrated. Please login instead."))
} else {
reportErrorHelper(requireContext(), Error("Failed preparing account for migration"))
}
dismissAllowingStateLoss()
}
}
}
model.observe(this) {
if (it.isFailed) {
reportErrorHelper(requireContext(), it.error!!)
dismissAllowingStateLoss()
} else {
doAsync {
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
val client = Client.create(httpClient, it.url.toString())
val etebase = EtebaseAccount.restore(client, it.etebaseSession!!, null)
uiThread {
fragmentManager?.commit {
replace(R.id.fragment_container, WizardCollectionsFragment.newInstance(accountV1, etebase))
addToBackStack(null)
}
dismissAllowingStateLoss()
}
}
} }
}
}
companion object {
fun newInstance(accountV1: Account, signupCredentials: SignupCredentials): SignupDoFragment {
val ret = SignupDoFragment()
ret.accountV1 = accountV1
ret.signupCredentials = signupCredentials
return ret
}
}
}
class LoginFragment() : Fragment() {
internal lateinit var editUserName: EditText
internal lateinit var editUrlPassword: TextInputLayout
internal lateinit var showAdvanced: CheckedTextView
internal lateinit var customServer: EditText
internal lateinit var accountV1: Account
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
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)
v.findViewById<View>(R.id.create_account).visibility = View.GONE
val login = v.findViewById<View>(R.id.login) as Button
login.setOnClickListener {
val credentials = validateLoginData()
if (credentials != null)
LoginDoFragment.newInstance(accountV1, credentials).show(fragmentManager!!, null)
}
val forgotPassword = v.findViewById<View>(R.id.forgot_password) as TextView
forgotPassword.setOnClickListener { WebViewActivity.openUrl(context!!, Constants.forgotPassword) }
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 validateLoginData(): LoginCredentials? {
var valid = true
val userName = editUserName.text.toString()
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
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) LoginCredentials(uri, userName, password) else null
}
companion object {
fun newInstance(accountV1: Account): LoginFragment {
val ret = LoginFragment()
ret.accountV1 = accountV1
return ret
}
}
}
class LoginDoFragment() : DialogFragment() {
private val model: ConfigurationViewModel by activityViewModels()
internal lateinit var accountV1: Account
internal lateinit var loginCredentials: LoginCredentials
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) {
val settings = AccountSettings(requireContext(), accountV1)
model.login(requireContext(), loginCredentials)
model.observe(this) {
if (it.isFailed) {
reportErrorHelper(requireContext(), it.error!!)
dismissAllowingStateLoss()
} else {
doAsync {
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
val client = Client.create(httpClient, it.url.toString())
val etebase = EtebaseAccount.restore(client, it.etebaseSession!!, null)
uiThread {
fragmentManager?.commit {
replace(R.id.fragment_container, WizardCollectionsFragment.newInstance(accountV1, etebase))
addToBackStack(null)
}
dismissAllowingStateLoss()
}
}
} }
}
}
companion object {
fun newInstance(accountV1: Account, loginCredentials: LoginCredentials): LoginDoFragment {
val ret = LoginDoFragment()
ret.accountV1 = accountV1
ret.loginCredentials = loginCredentials
return ret
}
}
}
class WizardCollectionsFragment() : Fragment() {
private val loadingModel: LoadingViewModel by viewModels()
private lateinit var info: AccountActivity.AccountInfo
private val migrateJournals = HashMap<String, AccountActivity.CollectionListItemInfo>()
internal lateinit var accountV1: Account
internal lateinit var etebase: EtebaseAccount
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.migrate_v2_collections, container, false)
if (savedInstanceState == null) {
if (container != null) {
initUi(inflater, ret)
}
}
return ret
}
private fun initUi(inflater: LayoutInflater, v: View) {
v.findViewById<Button>(R.id.button_create).setOnClickListener {
MigrateCollectionsDoFragment.newInstance(etebase, this.migrateJournals).show(parentFragmentManager, null)
}
v.findViewById<Button>(R.id.button_skip).setOnClickListener {
activity?.finish()
}
loadAccount(v)
}
private fun loadAccount(v: View) {
val account = accountV1
info = AccountActivity.AccountInfo()
val data = (requireContext().applicationContext as App).data
loadingModel.setLoading(true)
doAsync {
try {
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
val service = serviceEntity.type!!
when (service) {
CollectionInfo.Type.ADDRESS_BOOK -> {
info.carddav = AccountActivity.AccountInfo.ServiceInfo()
info.carddav!!.infos = getLegacyJournals(data, serviceEntity)
val accountManager = AccountManager.get(context)
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
val addressBook = LocalAddressBook(requireContext(), 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 = AccountActivity.AccountInfo.ServiceInfo()
info.caldav!!.infos = getLegacyJournals(data, serviceEntity)
}
CollectionInfo.Type.TASKS -> {
info.taskdav = AccountActivity.AccountInfo.ServiceInfo()
info.taskdav!!.infos = getLegacyJournals(data, serviceEntity)
}
}
}
} finally {
uiThread {
loadingModel.setLoading(false)
}
}
uiThread {
if (info.carddav != null) {
val infos = info.carddav!!.infos!!
val listCardDAV = v.findViewById<View>(R.id.address_books) as ListView
val adapter = CollectionListAdapter(requireContext(), account)
adapter.addAll(infos)
listCardDAV.adapter = adapter
listCardDAV.setOnItemClickListener { adapterView, view, i, l ->
val infoItem = infos.get(i)
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
} else {
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
}
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
}
}
if (info.caldav != null) {
val infos = info.caldav!!.infos!!
val listCalDAV = v.findViewById<View>(R.id.calendars) as ListView
val adapter = CollectionListAdapter(requireContext(), account)
adapter.addAll(infos)
listCalDAV.adapter = adapter
listCalDAV.setOnItemClickListener { adapterView, view, i, l ->
val infoItem = infos.get(i)
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
} else {
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
}
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
}
}
if (info.taskdav != null) {
val infos = info.taskdav!!.infos!!
val listTaskDAV = v.findViewById<View>(R.id.tasklists) as ListView
val adapter = CollectionListAdapter(requireContext(), account)
adapter.addAll(infos)
listTaskDAV.adapter = adapter
listTaskDAV.setOnItemClickListener { adapterView, view, i, l ->
val infoItem = infos.get(i)
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
} else {
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
}
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
}
}
}
}
}
private fun getLegacyJournals(data: MyEntityDataStore, serviceEntity: ServiceEntity): List<AccountActivity.CollectionListItemInfo> {
return JournalEntity.getJournals(data, serviceEntity).map {
val info = it.info
val isAdmin = it.isOwner(accountV1.name)
AccountActivity.CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description
?: "", info.color, it.isReadOnly, isAdmin, info)
}
}
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<AccountActivity.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)
v!!.findViewById<View>(R.id.sync).visibility = View.VISIBLE
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
tv = v.findViewById<View>(R.id.description) as TextView
if (TextUtils.isEmpty(info.description))
tv.visibility = View.GONE
else {
tv.visibility = View.VISIBLE
tv.text = info.description
}
val vColor = v.findViewById<View>(R.id.color)
if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
vColor.visibility = View.GONE
} else {
vColor.setBackgroundColor(info.color ?: LocalCalendar.defaultColor)
}
val readOnly = v.findViewById<View>(R.id.read_only)
readOnly.visibility = if (info.isReadOnly) View.VISIBLE else View.GONE
val shared = v.findViewById<View>(R.id.shared)
val isOwner = info.isAdmin
shared.visibility = if (isOwner) View.GONE else View.VISIBLE
return v
}
}
companion object {
fun newInstance(accountV1: Account, etebase: EtebaseAccount): WizardCollectionsFragment {
val ret = WizardCollectionsFragment()
ret.accountV1 = accountV1
ret.etebase = etebase
return ret
}
}
}
class MigrateCollectionsDoFragment : DialogFragment() {
private val configurationModel: ConfigurationViewModel by activityViewModels()
private lateinit var progress: ProgressDialog
private val CHUNK_SIZE = 20
internal lateinit var etebase: EtebaseAccount
internal lateinit var migrateJournals: HashMap<String, AccountActivity.CollectionListItemInfo>
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
progress = ProgressDialog(activity)
progress.setTitle(R.string.migrate_v2_wizard_migrate_title)
progress.setMessage(getString(R.string.migrate_v2_wizard_migrate_title))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
return progress
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
migrate()
}
}
private fun migrate() {
val data = (requireContext().applicationContext as App).data
doAsync {
try {
val total = migrateJournals.size
var malformed = 0
var badMtime = 0
var i = 1
val colMgr = etebase.collectionManager
for (itemInfo in migrateJournals.values) {
uiThread {
progress.setMessage(getString(R.string.migrate_v2_wizard_migrate_progress, i, total))
}
val colType = when (itemInfo.enumType) {
CollectionInfo.Type.ADDRESS_BOOK -> Constants.ETEBASE_TYPE_ADDRESS_BOOK
CollectionInfo.Type.CALENDAR -> Constants.ETEBASE_TYPE_CALENDAR
CollectionInfo.Type.TASKS -> Constants.ETEBASE_TYPE_TASKS
}
val colMeta = ItemMetadata()
colMeta.name = itemInfo.displayName
colMeta.description = itemInfo.description
if (itemInfo.color != null) {
colMeta.color = String.format("#%06X", 0xFFFFFF and itemInfo.color)
}
colMeta.mtime = System.currentTimeMillis()
val collection = colMgr.create(colType, colMeta, "")
colMgr.upload(collection)
val migratedItems = HashMap<String, Item>()
val journalEntity = JournalModel.Journal.fetch(data, itemInfo.legacyInfo!!.getServiceEntity(data), itemInfo.uid)
val entries = data.select(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.asc()).get().toList()
val itemMgr = colMgr.getItemManager(collection)
var itemDone = 0
val toPush = LinkedList<Item>()
for (entry in entries) {
itemDone++
val inputReader = StringReader(entry.content.content)
val uid: String?
var lastModified: Long?
when (itemInfo.enumType) {
CollectionInfo.Type.ADDRESS_BOOK -> {
val contact = Contact.fromReader(inputReader, null)[0]
uid = contact.uid
lastModified = null
}
CollectionInfo.Type.CALENDAR -> {
val event = Event.eventsFromReader(inputReader)[0]
uid = event.uid
lastModified = event.lastModified?.dateTime?.time
}
CollectionInfo.Type.TASKS -> {
val task = Task.tasksFromReader(inputReader)[0]
uid = task.uid
lastModified = task.lastModified
}
}
if (uid == null) {
malformed++
continue
}
if (lastModified == null) {
// When we can't set mtime, set to the item's position in the change log so we at least maintain EteSync 1.0 ordering.
lastModified = System.currentTimeMillis() + itemDone
badMtime++
}
val item: Item
if (migratedItems.containsKey(uid)) {
val tmp = migratedItems.get(uid)!!
// We need to clone the item so we can push multiple versions at once
item = itemMgr.cacheLoad(itemMgr.cacheSaveWithContent(tmp))
item.setContent(entry.content.content)
val meta = item.meta
meta.mtime = lastModified
item.meta = meta
} else {
val meta = ItemMetadata()
meta.mtime = lastModified
meta.name = uid
item = itemMgr.create(meta, entry.content.content)
migratedItems.set(uid, item)
}
if (entry.content.isAction(SyncEntry.Actions.DELETE)) {
item.delete()
}
toPush.add(item)
if (toPush.size == CHUNK_SIZE) {
uiThread {
progress.setMessage(context?.getString(R.string.migrate_v2_wizard_migrate_progress, i, total) + "\n" +
getString(R.string.migrate_v2_wizard_migrate_progress_entries, itemDone, entries.size))
}
itemMgr.batch(toPush.toTypedArray())
toPush.clear()
}
}
if (toPush.size > 0) {
itemMgr.batch(toPush.toTypedArray())
}
i++;
}
uiThread {
var message = getString(R.string.migrate_v2_wizard_migrate_progress_done)
if (malformed > 0) {
message += "\n\n" + getString(R.string.migrate_v2_wizard_migrate_progress_done_malformed, malformed)
}
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.migrate_v2_wizard_migrate_title)
.setMessage(message)
.setPositiveButton(android.R.string.yes) { _, _ -> }
.setOnDismissListener {
requireFragmentManager().commit {
replace(android.R.id.content, CreateAccountFragment.newInstance(configurationModel.account.value!!))
addToBackStack(null)
}
dismissAllowingStateLoss()
}
.show()
}
} catch (e: Exception) {
uiThread {
reportErrorHelper(requireContext(), e)
dismissAllowingStateLoss()
}
}
}
}
companion object {
fun newInstance(etebase: EtebaseAccount,
migrateJournals: HashMap<String, AccountActivity.CollectionListItemInfo>): MigrateCollectionsDoFragment {
val ret = MigrateCollectionsDoFragment()
ret.etebase = etebase
ret.migrateJournals = migrateJournals
return ret
}
}
}

View File

@ -94,7 +94,7 @@ class StartupDialogFragment : DialogFragment() {
// Vendor specific bugs
val manu = Build.MANUFACTURER
if (!HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS) && (manu.equals("Xiaomi", ignoreCase = true) || manu.equals("Huawei", ignoreCase = true)) && !Build.DISPLAY.contains("lineage")) {
if (!HintManager.getHintSeen(context, HINT_VENDOR_SPECIFIC_BUGS) && (manu.equals("Xiaomi", ignoreCase = true) || manu.equals("Huawei", ignoreCase = true)) && !Build.DISPLAY.contains("lineage")) {
dialogs.add(StartupDialogFragment.instantiate(Mode.VENDOR_SPECIFIC_BUGS))
}

View File

@ -31,7 +31,7 @@ class WebViewActivity : BaseActivity() {
mToolbar = supportActionBar
mToolbar!!.setDisplayHomeAsUpEnabled(true)
var uri = intent.getParcelableExtra<Uri>(KEY_URL)
var uri = intent.getParcelableExtra<Uri>(KEY_URL)!!
uri = addQueryParams(uri)
mWebView = findViewById<View>(R.id.webView) as WebView
mProgressBar = findViewById<View>(R.id.progressBar) as ProgressBar
@ -166,7 +166,7 @@ class WebViewActivity : BaseActivity() {
fun openUrl(context: Context, uri: Uri) {
if (isAllowedUrl(uri)) {
val intent = Intent(context, WebViewActivity::class.java)
intent.putExtra(WebViewActivity.KEY_URL, uri)
intent.putExtra(KEY_URL, uri)
context.startActivity(intent)
} else {
try {
@ -191,10 +191,22 @@ class WebViewActivity : BaseActivity() {
}
private fun isAllowedUrl(uri: Uri): Boolean {
val allowedUris = arrayOf(Constants.faqUri, Constants.helpUri, Constants.registrationUrl, Constants.dashboard, Constants.webUri.buildUpon().appendEncodedPath("tos/").build(), Constants.webUri.buildUpon().appendEncodedPath("about/").build())
val allowedUris = arrayOf(
Constants.faqUri,
Constants.helpUri,
Constants.registrationUrl,
Constants.dashboard,
Constants.webUri.buildUpon().appendEncodedPath("tos/").build(),
Constants.webUri.buildUpon().appendEncodedPath("about/").build(),
Constants.pricing,
)
val accountsUri = Constants.webUri.buildUpon().appendEncodedPath("accounts/").build()
return allowedUris(allowedUris, uri) || uri.host == accountsUri.host && uri.path!!.startsWith(accountsUri.path!!)
return allowedUris(allowedUris, uri) || (
uri.host == accountsUri.host && uri.path!!.startsWith(accountsUri.path!!)
) || (
uri.host == Constants.etebaseDashboardPrefix.host && uri.path!!.startsWith(Constants.etebaseDashboardPrefix.path!!)
)
}
}
}

View File

@ -11,7 +11,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.CollectionManager
import com.etebase.client.CollectionMetadata
import com.etebase.client.ItemMetadata
import com.etesync.syncadapter.*
import com.etesync.syncadapter.ui.BaseActivity
import org.jetbrains.anko.doAsync
@ -30,7 +30,7 @@ class CollectionActivity() : BaseActivity() {
val colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)
val colType = intent.extras!!.getString(EXTRA_COLLECTION_TYPE)
setContentView(R.layout.etebase_collection_activity)
setContentView(R.layout.etebase_fragment_activity)
if (savedInstanceState == null) {
model.loadAccount(this, account)
@ -47,11 +47,12 @@ class CollectionActivity() : BaseActivity() {
} else if (colType != null) {
model.observe(this) {
doAsync {
val meta = CollectionMetadata(colType, "")
val cachedCollection = CachedCollection(it.colMgr.create(meta, ""), meta)
val meta = ItemMetadata()
meta.name = ""
val cachedCollection = CachedCollection(it.colMgr.create(colType, meta, ""), meta, colType)
uiThread {
supportFragmentManager.commit {
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection, true))
replace(R.id.fragment_container, EditCollectionFragment.newInstance(cachedCollection, true))
}
}
}

View File

@ -37,10 +37,12 @@ import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Future
class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() {
class CollectionItemFragment : Fragment() {
private val model: AccountViewModel by activityViewModels()
private val collectionModel: CollectionViewModel by activityViewModels()
private lateinit var cachedItem: CachedItem
private var emailInvitationEvent: Event? = null
private var emailInvitationEventString: String? = null
@ -101,7 +103,7 @@ class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() {
val context = requireContext()
val account = accountHolder.account
val cachedCol = collectionModel.value!!
when (cachedCol.meta.collectionType) {
when (cachedCol.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!
val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, cachedCol.col.uid)!!
@ -156,6 +158,14 @@ class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() {
.create()
dialog.show()
}
companion object {
fun newInstance(cachedItem: CachedItem): CollectionItemFragment {
val ret = CollectionItemFragment()
ret.cachedItem = cachedItem
return ret
}
}
}
private class TabsAdapter(fm: FragmentManager, private val mainFragment: CollectionItemFragment, private val context: Context, private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : FragmentPagerAdapter(fm) {
@ -177,17 +187,18 @@ private class TabsAdapter(fm: FragmentManager, private val mainFragment: Collect
override fun getItem(position: Int): Fragment {
return if (position == 0) {
PrettyFragment(mainFragment, cachedCollection, cachedItem.content)
PrettyFragment.newInstance(mainFragment, cachedCollection, cachedItem.content)
} else if (position == 1) {
TextFragment(cachedItem.content)
TextFragment.newInstance(cachedItem.content)
} else {
ItemRevisionsListFragment(cachedCollection, cachedItem)
ItemRevisionsListFragment.newInstance(cachedCollection, cachedItem)
}
}
}
class TextFragment(private val content: String) : Fragment() {
class TextFragment : Fragment() {
private lateinit var content: String
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.text_fragment, container, false)
@ -197,15 +208,26 @@ class TextFragment(private val content: String) : Fragment() {
return v
}
companion object {
fun newInstance(content: String): TextFragment {
val ret = TextFragment()
ret.content = content
return ret
}
}
}
class PrettyFragment(private val mainFragment: CollectionItemFragment, private val cachedCollection: CachedCollection, private val content: String) : Fragment() {
class PrettyFragment : Fragment() {
private var asyncTask: Future<Unit>? = null
private lateinit var mainFragment: CollectionItemFragment
private lateinit var cachedCollection: CachedCollection
private lateinit var content: String
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var v: View? = null
when (cachedCollection.meta.collectionType) {
when (cachedCollection.collectionType) {
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
v = inflater.inflate(R.layout.contact_info, container, false)
asyncTask = loadContactTask(v)
@ -251,7 +273,13 @@ class PrettyFragment(private val mainFragment: CollectionItemFragment, private v
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))
val dtStart = event.dtStart?.date?.time
val dtEnd = event.dtEnd?.date?.time
if ((dtStart == null) || (dtEnd == null)) {
setTextViewText(view, R.id.when_datetime, getString(R.string.loading_error_title))
} else {
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(dtStart, dtEnd, event.isAllDay(), context))
}
setTextViewText(view, R.id.where, event.location)
@ -476,6 +504,14 @@ class PrettyFragment(private val mainFragment: CollectionItemFragment, private v
}
companion object {
fun newInstance(mainFragment: CollectionItemFragment, cachedCollection: CachedCollection, content: String): PrettyFragment {
val ret = PrettyFragment()
ret.mainFragment= mainFragment
ret.cachedCollection = cachedCollection
ret.content = content
return ret
}
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)

View File

@ -54,9 +54,10 @@ class CollectionMembersFragment : Fragment() {
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
val meta = cachedCollection.meta
val collectionType = cachedCollection.collectionType
val colorSquare = v.findViewById<View>(R.id.color)
val color = LocalCalendar.parseColor(meta.color)
when (meta.collectionType) {
when (collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
colorSquare.setBackgroundColor(color)
}
@ -104,7 +105,7 @@ class CollectionMembersFragment : Fragment() {
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)
val frag = AddMemberFragment.newInstance(model.value!!, collectionModel.value!!, username, if (readOnly) CollectionAccessLevel.ReadOnly else CollectionAccessLevel.ReadWrite)
frag.show(childFragmentManager, null)
}
.setNegativeButton(android.R.string.no) { _, _ -> }
@ -113,7 +114,12 @@ class CollectionMembersFragment : Fragment() {
}
}
class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: CollectionAccessLevel) : DialogFragment() {
class AddMemberFragment : DialogFragment() {
private lateinit var accountHolder: AccountHolder
private lateinit var cachedCollection: CachedCollection
private lateinit var username: String
private lateinit var accessLevel: CollectionAccessLevel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(context)
progress.setTitle(R.string.collection_members_adding)
@ -173,4 +179,16 @@ class AddMemberFragment(private val accountHolder: AccountHolder, private val ca
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
dismiss()
}
companion object {
fun newInstance(accountHolder: AccountHolder, cachedCollection: CachedCollection,
username: String, accessLevel: CollectionAccessLevel): AddMemberFragment {
val ret = AddMemberFragment()
ret.accountHolder = accountHolder
ret.cachedCollection = cachedCollection
ret.username = username
ret.accessLevel = accessLevel
return ret
}
}
}

View File

@ -15,6 +15,7 @@ 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.log.Logger
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.BaseActivity
@ -23,12 +24,15 @@ 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() {
class EditCollectionFragment : 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()
private lateinit var cachedCollection: CachedCollection
private var isCreating: Boolean = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = inflater.inflate(R.layout.activity_create_collection, container, false)
setHasOptionsMenu(true)
@ -47,7 +51,7 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
cachedCollection.let {
var titleId: Int = R.string.create_calendar
if (isCreating) {
when (cachedCollection.meta.collectionType) {
when (cachedCollection.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
titleId = R.string.create_calendar
}
@ -75,7 +79,7 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
desc.setText(meta.description)
val colorSquare = v.findViewById<View>(R.id.color)
when (cachedCollection.meta.collectionType) {
when (cachedCollection.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
title.setHint(R.string.create_calendar_display_name_hint)
@ -167,11 +171,14 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
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()
Logger.log.warning(e.localizedMessage)
context?.let { context ->
AlertDialog.Builder(context)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
}
} finally {
uiThread {
@ -200,7 +207,7 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
meta.mtime = System.currentTimeMillis()
if (ok) {
when (meta.collectionType) {
when (cachedCollection.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> {
val view = v.findViewById<View>(R.id.color)
val color = (view.background as ColorDrawable).color
@ -231,11 +238,14 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
}
} 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()
val context = context
if (context != null) {
AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.exception)
.setMessage(e.localizedMessage)
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
}
}
} finally {
uiThread {
@ -256,4 +266,13 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri
}
collectionModel.loadCollection(model.value!!, col.uid)
}
companion object {
fun newInstance(cachedCollection: CachedCollection, isCreating: Boolean = false): EditCollectionFragment {
val ret = EditCollectionFragment()
ret.cachedCollection = cachedCollection
ret.isCreating = isCreating
return ret
}
}
}

View File

@ -57,14 +57,14 @@ class ImportCollectionFragment : Fragment() {
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) {
if (cachedCollection.collectionType == Constants.ETEBASE_TYPE_CALENDAR) {
parentFragmentManager.commit {
replace(R.id.fragment_container, LocalCalendarImportFragment(accountHolder.account, cachedCollection.col.uid))
replace(R.id.fragment_container, LocalCalendarImportFragment.newInstance(accountHolder.account, cachedCollection.col.uid))
addToBackStack(null)
}
} else if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK) {
} else if (cachedCollection.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK) {
parentFragmentManager.commit {
replace(R.id.fragment_container, LocalContactImportFragment(accountHolder.account, cachedCollection.col.uid))
replace(R.id.fragment_container, LocalContactImportFragment.newInstance(accountHolder.account, cachedCollection.col.uid))
addToBackStack(null)
}
}
@ -72,7 +72,7 @@ class ImportCollectionFragment : Fragment() {
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_select_account)
}
if (collectionModel.value!!.meta.collectionType == Constants.ETEBASE_TYPE_TASKS) {
if (collectionModel.value!!.collectionType == Constants.ETEBASE_TYPE_TASKS) {
card.visibility = View.GONE
}
}

View File

@ -18,7 +18,7 @@ class InvitationsActivity : BaseActivity() {
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
setContentView(R.layout.etebase_collection_activity)
setContentView(R.layout.etebase_fragment_activity)
if (savedInstanceState == null) {
model.loadAccount(this, account)

View File

@ -5,7 +5,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
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
@ -19,6 +21,7 @@ import com.etebase.client.FetchOptions
import com.etebase.client.SignedInvitation
import com.etebase.client.Utils
import com.etesync.syncadapter.R
import com.etesync.syncadapter.syncadapter.requestSync
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
@ -88,6 +91,10 @@ class InvitationsListFragment : ListFragment(), AdapterView.OnItemClickListener
}
.setPositiveButton(R.string.invitations_accept) { dialogInterface, i ->
invitationsModel.accept(model.value!!, invitation)
val applicationContext = activity?.applicationContext
if (applicationContext != null) {
requestSync(applicationContext, model.value!!.account)
}
}
.show()
return

View File

@ -28,11 +28,14 @@ import java.util.*
import java.util.concurrent.Future
class ItemRevisionsListFragment(private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : ListFragment(), AdapterView.OnItemClickListener {
class ItemRevisionsListFragment : ListFragment(), AdapterView.OnItemClickListener {
private val model: AccountViewModel by activityViewModels()
private val revisionsModel: RevisionsViewModel by viewModels()
private var state: Parcelable? = null
private lateinit var cachedCollection: CachedCollection
private lateinit var cachedItem: CachedItem
private var emptyTextView: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -83,7 +86,7 @@ class ItemRevisionsListFragment(private val cachedCollection: CachedCollection,
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))
replace(R.id.fragment_container, CollectionItemFragment.newInstance(item))
addToBackStack(null)
}
}
@ -95,9 +98,9 @@ class ItemRevisionsListFragment(private val cachedCollection: CachedCollection,
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
val item = getItem(position)
val item = getItem(position)!!
setItemView(v, cachedCollection.meta.collectionType, item)
setItemView(v, cachedCollection.collectionType, item)
/* FIXME: handle entry error:
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()
@ -110,6 +113,15 @@ class ItemRevisionsListFragment(private val cachedCollection: CachedCollection,
return v
}
}
companion object {
fun newInstance(cachedCollection: CachedCollection, cachedItem: CachedItem): ItemRevisionsListFragment {
val ret = ItemRevisionsListFragment()
ret.cachedCollection = cachedCollection
ret.cachedItem = cachedItem
return ret
}
}
}

View File

@ -71,7 +71,7 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
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))
replace(R.id.fragment_container, CollectionItemFragment.newInstance(item))
addToBackStack(EditCollectionFragment::class.java.name)
}
}
@ -83,9 +83,9 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
if (v == null)
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
val item = getItem(position)
val item = getItem(position)!!
setItemView(v, cachedCollection.meta.collectionType, item)
setItemView(v, cachedCollection.collectionType, item)
/* FIXME: handle entry error:
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()

View File

@ -16,9 +16,10 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import com.etebase.client.Collection
import com.etebase.client.CollectionMetadata
import com.etebase.client.FetchOptions
import com.etebase.client.ItemMetadata
import com.etebase.client.exceptions.EtebaseException
import com.etesync.syncadapter.Constants.*
import com.etesync.syncadapter.R
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.BaseActivity
@ -34,7 +35,7 @@ class NewAccountWizardActivity : BaseActivity() {
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
setContentView(R.layout.etebase_collection_activity)
setContentView(R.layout.etebase_fragment_activity)
if (savedInstanceState == null) {
setTitle(R.string.account_wizard_collections_title)
@ -106,7 +107,7 @@ class WizardCheckFragment : Fragment() {
loadingModel.setLoading(true)
doAsync {
try {
val collections = colMgr.list(FetchOptions().limit(1))
val collections = colMgr.list(COLLECTION_TYPES, FetchOptions().limit(1))
uiThread {
if (collections.data.size > 0) {
activity?.finish()
@ -119,9 +120,6 @@ class WizardCheckFragment : Fragment() {
} catch (e: Exception) {
uiThread {
reportErrorHelper(requireContext(), e)
}
} finally {
uiThread {
loadingModel.setLoading(false)
}
}
@ -175,16 +173,17 @@ class WizardFragment : Fragment() {
doAsync {
try {
val baseMeta = listOf(
Pair("etebase.vcard", "My Contacts"),
Pair("etebase.vevent", "My Calendar"),
Pair("etebase.vtodo", "My Tasks"),
Pair(ETEBASE_TYPE_ADDRESS_BOOK, "My Contacts"),
Pair(ETEBASE_TYPE_CALENDAR, "My Calendar"),
Pair(ETEBASE_TYPE_TASKS, "My Tasks"),
)
baseMeta.forEach {
val meta = CollectionMetadata(it.first, it.second)
val meta = ItemMetadata()
meta.name = it.second
meta.mtime = System.currentTimeMillis()
val col = colMgr.create(meta, "")
val col = colMgr.create(it.first, meta, "")
uploadCollection(accountHolder, col)
}
requestSync(requireContext(), accountHolder.account)

View File

@ -17,6 +17,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckedTextView
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
@ -32,10 +33,8 @@ 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.etesync.syncadapter.ui.WebViewActivity
import com.etesync.syncadapter.ui.setup.*
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import net.cachapa.expandablelayout.ExpandableLayout
@ -45,7 +44,9 @@ 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() {
class SignupFragment : Fragment() {
internal var initialUsername: String? = null
internal var initialPassword: String? = null
internal lateinit var editUserName: TextInputLayout
internal lateinit var editEmail: TextInputLayout
internal lateinit var editPassword: TextInputLayout
@ -62,6 +63,9 @@ class SignupFragment(private val initialUsername: String?, private val initialPa
editPassword = v.findViewById(R.id.url_password)
showAdvanced = v.findViewById(R.id.show_advanced)
customServer = v.findViewById(R.id.custom_server)
v.findViewById<TextView>(R.id.trial_notice).setOnClickListener {
WebViewActivity.openUrl(requireContext(), Constants.pricing)
}
if (savedInstanceState == null) {
editUserName.editText?.setText(initialUsername ?: "")
@ -71,7 +75,7 @@ class SignupFragment(private val initialUsername: String?, private val initialPa
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()))
replace(android.R.id.content, LoginCredentialsFragment.newInstance(editUserName.editText?.text.toString(), editPassword.editText?.text.toString()))
}
}
@ -79,7 +83,7 @@ class SignupFragment(private val initialUsername: String?, private val initialPa
createAccount.setOnClickListener {
val credentials = validateData()
if (credentials != null) {
SignupDoFragment(credentials).show(requireFragmentManager(), null)
SignupDoFragment.newInstance(credentials).show(requireFragmentManager(), null)
}
}
@ -144,13 +148,24 @@ class SignupFragment(private val initialUsername: String?, private val initialPa
return if (valid) SignupCredentials(uri, userName, email, password) else null
}
companion object {
fun newInstance(initialUsername: String?, initialPassword: String?): SignupFragment {
val ret = SignupFragment()
ret.initialUsername = initialUsername
ret.initialPassword = initialPassword
return ret
}
}
}
class SignupDoFragment(private val signupCredentials: SignupCredentials) : DialogFragment() {
class SignupDoFragment: DialogFragment() {
private val model: ConfigurationViewModel by viewModels()
private lateinit var signupCredentials: SignupCredentials
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
progress.setTitle(R.string.setting_up_encryption)
@ -182,10 +197,18 @@ class SignupDoFragment(private val signupCredentials: SignupCredentials) : Dialo
}
}
}
companion object {
fun newInstance(signupCredentials: SignupCredentials): SignupDoFragment {
val ret = SignupDoFragment()
ret.signupCredentials = signupCredentials
return ret
}
}
}
class ConfigurationViewModel : ViewModel() {
private val account = MutableLiveData<BaseConfigurationFinder.Configuration>()
val account = MutableLiveData<BaseConfigurationFinder.Configuration>()
private var asyncTask: Future<Unit>? = null
fun signup(context: Context, credentials: SignupCredentials) {
@ -216,6 +239,34 @@ class ConfigurationViewModel : ViewModel() {
}
}
// We just need it for the migration - maybe merge it with login later on
fun login(context: Context, credentials: LoginCredentials) {
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 etebase = Account.login(client, credentials.userName, 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)
}

View File

@ -4,6 +4,7 @@ import android.content.DialogInterface
import android.os.Bundle
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -68,7 +69,7 @@ class ViewCollectionFragment : Fragment() {
val colorSquare = container.findViewById<View>(R.id.color)
val color = LocalCalendar.parseColor(meta.color)
when (meta.collectionType) {
when (cachedCollection.collectionType) {
Constants.ETEBASE_TYPE_CALENDAR -> {
colorSquare.setBackgroundColor(color)
}
@ -108,13 +109,17 @@ class ViewCollectionFragment : Fragment() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val cachedCollection = collectionModel.value!!
val cachedCollection = collectionModel.value
if (cachedCollection == null) {
Toast.makeText(context, R.string.loading_error_title, Toast.LENGTH_LONG).show()
return super.onOptionsItemSelected(item)
}
when (item.itemId) {
R.id.on_edit -> {
if (cachedCollection.col.accessLevel == CollectionAccessLevel.Admin) {
parentFragmentManager.commit {
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection))
replace(R.id.fragment_container, EditCollectionFragment.newInstance(cachedCollection))
addToBackStack(EditCollectionFragment::class.java.name)
}
} else {

View File

@ -46,13 +46,13 @@ class ImportActivity : BaseActivity(), SelectImportMethod, DialogInterface {
if (info.enumType == CollectionInfo.Type.CALENDAR) {
supportFragmentManager.beginTransaction()
.replace(android.R.id.content,
LocalCalendarImportFragment.newInstance(account, info))
LocalCalendarImportFragment.newInstance(account, info.uid!!))
.addToBackStack(LocalCalendarImportFragment::class.java.name)
.commit()
} else if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
supportFragmentManager.beginTransaction()
.replace(android.R.id.content,
LocalContactImportFragment.newInstance(account, info))
LocalContactImportFragment.newInstance(account, info.uid!!))
.addToBackStack(LocalContactImportFragment::class.java.name)
.commit()
}

View File

@ -35,7 +35,10 @@ import java.io.InputStream
import java.io.InputStreamReader
class ImportFragment(private val account: Account, private val uid: String, private val enumType: CollectionInfo.Type) : DialogFragment() {
class ImportFragment : DialogFragment() {
private lateinit var account: Account
private lateinit var uid: String
private lateinit var enumType: CollectionInfo.Type
private var inputStream: InputStream? = null
@ -234,9 +237,10 @@ class ImportFragment(private val account: Account, private val uid: String, priv
val localCalendar: LocalCalendar?
try {
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, uid!!)
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, uid)
if (localCalendar == null) {
throw FileNotFoundException("Failed to load local resource.")
result.e = FileNotFoundException("Failed to load local resource.")
return result
}
} catch (e: CalendarStorageException) {
Logger.log.info("Fail" + e.localizedMessage)
@ -292,9 +296,10 @@ class ImportFragment(private val account: Account, private val uid: String, priv
provider?.let {
val localTaskList: LocalTaskList?
try {
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, uid!!)
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, uid)
if (localTaskList == null) {
throw FileNotFoundException("Failed to load local resource.")
result.e = FileNotFoundException("Failed to load local resource.")
return result
}
} catch (e: FileNotFoundException) {
Logger.log.info("Fail" + e.localizedMessage)
@ -341,9 +346,10 @@ class ImportFragment(private val account: Account, private val uid: String, priv
return result
}
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, uid!!)
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, uid)
if (localAddressBook == null) {
throw FileNotFoundException("Failed to load local address book.")
result.e = FileNotFoundException("Failed to load local address book.")
return result
}
for (contact in contacts.filter { contact -> !contact.group }) {
@ -432,17 +438,25 @@ class ImportFragment(private val account: Account, private val uid: String, priv
private val TAG_PROGRESS_MAX = "progressMax"
fun newInstance(account: Account, info: CollectionInfo): ImportFragment {
return ImportFragment(account, info.uid!!, info.enumType!!)
val ret = ImportFragment()
ret.account = account
ret.uid = info.uid!!
ret.enumType = info.enumType!!
return ret
}
fun newInstance(account: Account, cachedCollection: CachedCollection): ImportFragment {
val enumType = when (cachedCollection.meta.collectionType) {
val enumType = when (cachedCollection.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)
val ret = ImportFragment()
ret.account = account
ret.uid = cachedCollection.col.uid
ret.enumType = enumType
return ret
}
}
}

View File

@ -23,7 +23,10 @@ import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.resource.LocalEvent
class LocalCalendarImportFragment(private val account: Account, private val uid: String) : ListFragment() {
class LocalCalendarImportFragment : ListFragment() {
private lateinit var account: Account
private lateinit var uid: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
@ -249,8 +252,11 @@ class LocalCalendarImportFragment(private val account: Account, private val uid:
companion object {
fun newInstance(account: Account, info: CollectionInfo): LocalCalendarImportFragment {
return LocalCalendarImportFragment(account, info.uid!!)
fun newInstance(account: Account, uid: String): LocalCalendarImportFragment {
val ret = LocalCalendarImportFragment()
ret.account = account
ret.uid = uid
return ret
}
}
}

View File

@ -31,7 +31,10 @@ import com.etesync.syncadapter.resource.LocalGroup
import java.util.*
class LocalContactImportFragment(private val account: Account, private val uid: String) : Fragment() {
class LocalContactImportFragment : Fragment() {
private lateinit var account: Account
private lateinit var uid: String
private var recyclerView: RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -316,8 +319,11 @@ class LocalContactImportFragment(private val account: Account, private val uid:
companion object {
fun newInstance(account: Account, info: CollectionInfo): LocalContactImportFragment {
return LocalContactImportFragment(account, info.uid!!)
fun newInstance(account: Account, uid: String): LocalContactImportFragment {
val ret = LocalContactImportFragment()
ret.account = account
ret.uid = uid
return ret
}
}
}

View File

@ -40,7 +40,7 @@ class ResultFragment : DialogFragment() {
// dismiss
}
.setPositiveButton(android.R.string.yes) { dialog, which ->
ACRA.getErrorReporter().handleSilentException(result!!.e)
ACRA.getErrorReporter().handleException(result!!.e)
}
.create()
} else {

View File

@ -23,8 +23,10 @@ import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.ui.DebugInfoActivity
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallbacks<Configuration> {
class DetectConfigurationFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(activity)
@ -40,14 +42,22 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
super.onCreate(savedInstanceState)
Logger.log.fine("DetectConfigurationFragment: loading")
loaderManager.initLoader(0, arguments, this)
if (savedInstanceState == null) {
findConfiguration(requireArguments().getParcelable(ARG_LOGIN_CREDENTIALS)!!)
}
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Configuration> {
return ServerConfigurationLoader(requireContext(), args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
private fun findConfiguration(credentials: LoginCredentials) {
doAsync {
val data = BaseConfigurationFinder(requireContext(), credentials).findInitialConfiguration()
uiThread {
onLoadFinished(data)
}
}
}
override fun onLoadFinished(loader: Loader<Configuration>, data: Configuration?) {
private fun onLoadFinished(data: Configuration?) {
if (data != null) {
if (data.isFailed) {
Logger.log.warning("Failed login configuration ${data.error?.localizedMessage}")
@ -75,13 +85,10 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
dismissAllowingStateLoss()
}
override fun onLoaderReset(loader: Loader<Configuration>) {}
class NothingDetectedFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(activity!!)
return AlertDialog.Builder(requireActivity())
.setTitle(R.string.setting_up_encryption)
.setIcon(R.drawable.ic_error_dark)
.setMessage(requireArguments().getString(KEY_LOGS))
@ -104,17 +111,6 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
}
}
internal class ServerConfigurationLoader(context: Context, val credentials: LoginCredentials) : AsyncTaskLoader<Configuration>(context) {
override fun onStartLoading() {
forceLoad()
}
override fun loadInBackground(): Configuration? {
return BaseConfigurationFinder(context, credentials).findInitialConfiguration()
}
}
companion object {
protected val ARG_LOGIN_CREDENTIALS = "credentials"

View File

@ -30,7 +30,7 @@ class LoginActivity : BaseActivity() {
if (savedInstanceState == null)
// first call, add fragment
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, LoginCredentialsFragment(null, null))
.replace(android.R.id.content, LoginCredentialsFragment())
.commit()
}

View File

@ -47,7 +47,7 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
}
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?) {

View File

@ -29,13 +29,16 @@ import net.cachapa.expandablelayout.ExpandableLayout
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URI
class LoginCredentialsFragment(private val initialUsername: String?, private val initialPassword: String?) : Fragment() {
class LoginCredentialsFragment : Fragment() {
internal lateinit var editUserName: EditText
internal lateinit var editUrlPassword: TextInputLayout
internal lateinit var showAdvanced: CheckedTextView
internal lateinit var customServer: EditText
internal var initialUsername: String? = null
internal var initialPassword: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
@ -53,7 +56,7 @@ class LoginCredentialsFragment(private val initialUsername: String?, private val
val createAccount = v.findViewById<View>(R.id.create_account) as Button
createAccount.setOnClickListener {
parentFragmentManager.commit {
replace(android.R.id.content, SignupFragment(editUserName.text.toString(), editUrlPassword.editText?.text.toString()))
replace(android.R.id.content, SignupFragment.newInstance(editUserName.text.toString(), editUrlPassword.editText?.text.toString()))
}
}
@ -119,4 +122,13 @@ class LoginCredentialsFragment(private val initialUsername: String?, private val
return if (valid) LoginCredentials(uri, userName, password) else null
}
companion object {
fun newInstance(initialUsername: String?, initialPassword: String?): LoginCredentialsFragment {
val ret = LoginCredentialsFragment()
ret.initialUsername = initialUsername
ret.initialPassword = initialPassword
return ret
}
}
}

View File

@ -15,15 +15,6 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
fun emailSupportsAttachments(context: Context): Boolean {
return !arrayOf(
"ch.protonmail.android",
"de.tutao.tutanota"
).any{
packageInstalled(context, it)
}
}
class EventEmailInvitation constructor(val context: Context, val account: Account) {
fun createIntent(event: Event, icsContent: String): Intent? {
val intent = Intent(Intent.ACTION_SEND)

View File

@ -17,6 +17,16 @@
android:paddingLeft="12dp"
android:paddingRight="12dp">
<CheckBox
android:id="@+id/sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:focusableInTouchMode="false"
android:clickable="false"
android:layout_marginRight="4dp"
android:visibility="gone"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -0,0 +1,194 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:id="@+id/parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<androidx.cardview.widget.CardView
android:id="@+id/carddav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardElevation="8dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/carddav_menu"
style="@style/toolbar_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:theme="@style/toolbar_theme"
app:navigationIcon="@drawable/ic_people_light"
app:title="@string/settings_carddav"
tools:ignore="UnusedAttribute" />
<ProgressBar
android:id="@+id/carddav_refreshing"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone" />
<com.etesync.syncadapter.ui.widget.MaximizedListView
android:id="@+id/address_books"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/caldav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/caldav_menu"
style="@style/toolbar_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:theme="@style/toolbar_theme"
app:navigationIcon="@drawable/ic_event_light"
app:title="@string/settings_caldav"
tools:ignore="UnusedAttribute" />
<ProgressBar
android:id="@+id/caldav_refreshing"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone" />
<com.etesync.syncadapter.ui.widget.MaximizedListView
android:id="@+id/calendars"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="multipleChoice"
android:descendantFocusability="beforeDescendants" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/taskdav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/taskdav_menu"
style="@style/toolbar_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:theme="@style/toolbar_theme"
app:navigationIcon="@drawable/ic_task_light"
app:title="@string/settings_taskdav"
tools:ignore="UnusedAttribute" />
<ProgressBar
android:id="@+id/taskdav_refreshing"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone" />
<TextView
android:id="@+id/taskdav_opentasks_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:text="@string/account_tasks_not_showing"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone" />
<com.etesync.syncadapter.ui.widget.MaximizedListView
android:id="@+id/tasklists"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="multipleChoice"
android:descendantFocusability="beforeDescendants" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/buttons_holder"
style="@style/stepper_nav_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/button_skip"
style="@style/stepper_nav_button"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/skip" />
<Space
style="@style/stepper_nav_button"
android:layout_width="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/button_create"
style="@style/stepper_nav_button"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/create" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,73 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/migrate_v2_wizard_welcome_title"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/migrate_v2_wizard_welcome_body" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:gravity="center"
android:text="@string/migrate_v2_wizard_welcome_body_extra" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/stepper_nav_bar">
<Button
android:id="@+id/signup"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/login_signup"
style="@style/stepper_nav_button"/>
<Space
android:layout_width="0dp"
android:layout_weight="1"
style="@style/stepper_nav_button"/>
<Button
android:id="@+id/login"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/login_login"
style="@style/stepper_nav_button"/>
</LinearLayout>
</LinearLayout>

View File

@ -29,6 +29,15 @@
android:text="@string/signup_title"
android:layout_marginBottom="14dp"/>
<TextView
android:id="@+id/trial_notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="You are signing up for a free trial. Click here for pricing information."
android:background="@color/infoColor"
android:padding="14dp"
android:layout_marginBottom="14dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/user_name"
android:layout_width="match_parent"

View File

@ -29,6 +29,11 @@
android:title="@string/invitations_title"
app:showAsAction="never"/>
<item android:id="@+id/migration_v2"
android:title="@string/migrate_v2_wizard_welcome_title"
android:visible="false"
app:showAsAction="never"/>
<item android:id="@+id/delete_account"
android:title="@string/account_delete"
app:showAsAction="never"/>

View File

@ -89,7 +89,7 @@
<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_login">Anmelden</string>
<string name="login_signup">Registrieren</string>
<string name="login_finish">Fertigstellen</string>
<string name="login_back">Zurück</string>

View File

@ -166,7 +166,7 @@
<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
\n%s
\nQuand : %s
\nOù : %s
\nParticipants : %s
@ -192,4 +192,7 @@
<string name="notification_channel_debugging">Débogage</string>
<string name="notification_channel_crash_reports">Rapports de plantage</string>
<string name="app_name">EteSync</string>
<string name="retry">Réessayer</string>
<string name="skip">Passer</string>
<string name="create">Créer</string>
</resources>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_license_info_no_warranty">Program ini dilengkapi dengan BENAR-BENAR TIDAK ADA GARANSI. Ini adalah perangkat lunak gratis, dan Anda dipersilakan untuk mendistribusikannya dalam kondisi tertentu.</string>
<string name="about_license_terms">Persyaratan lisensi</string>
<string name="startup_vendor_specific_bugs_open_faq">Buka FAQ</string>
<string name="startup_vendor_specific_bugs_message">EteSync telah mendeteksi Anda menggunakan versi Android yang mungkin berisi bug khusus vendor.
\nSilakan lihat FAQ untuk informasi lebih lanjut.</string>
<string name="startup_vendor_specific_bugs">Potensi Kesalahan dari Vendor</string>
<string name="startup_development_version_give_feedback">Berikan umpan balik</string>
<string name="startup_development_version_message">Ini adalah versi pengembangan dari EteSync. Ketahuilah bahwa hal-hal yang mungkin tidak bekerja seperti yang diharapkan. Tolong beri kami umpan balik yang konstruktif untuk meningkatkan EteSync.</string>
<string name="startup_development_version">Rilis Pratinjau EteSync</string>
<string name="startup_dont_show_again">Jangan tampilkan lagi</string>
<string name="startup_battery_optimization_disable">Matikan untuk EteSync</string>
<string name="startup_battery_optimization">Pengoptimalan Baterai</string>
<string name="startup_battery_optimization_message">Android dapat menonaktifkan/mengurangi sinkronisasi EteSync setelah beberapa hari. Untuk mencegah hal ini, matikan pengoptimalan baterai.</string>
<string name="tourguide_title">Tahukah kamu\?</string>
<string name="crash_email_body">Jika memungkinkan, harap sertakan informasi lain yang relevan seperti apa yang Anda lakukan yang memicu kesalahan ini.</string>
<string name="crash_title">EteSync telah rusak!</string>
<string name="notification_channel_sync_status_desc">Pesan status informasi seperti ringkasan perubahan sinkronisasi</string>
<string name="notification_channel_sync_status">Pesan status</string>
<string name="notification_channel_sync_warnings_desc">Masalah sinkronisasi non-fatal seperti perubahan koleksi baca-saja yang diberhentikan</string>
<string name="notification_channel_sync_warnings">Peringatan sinkronisasi</string>
<string name="notification_channel_sync_errors_desc">Kesalahan penting yang menghentikan sinkronisasi seperti balasan server yang tidak terduga</string>
<string name="notification_channel_sync_errors">Kesalahan sinkronisasi</string>
<string name="notification_channel_sync">Sinkronisasi</string>
<string name="notification_channel_general">Pesan penting lainnya</string>
<string name="notification_channel_debugging">Debugging</string>
<string name="notification_channel_crash_reports">Laporan Kerusakan</string>
<string name="retry">Mencoba kembali</string>
<string name="skip">Lewati</string>
<string name="create">Buat</string>
<string name="send">Kirim</string>
<string name="please_wait">Tunggu sebentar…</string>
<string name="manage_accounts">Kelola akun</string>
<string name="help">Bantuan</string>
<string name="address_books_authority_title">Buku alamat</string>
<string name="account_title_address_book">Buku alamat EteSync</string>
<string name="app_name">EteSync</string>
</resources>

View File

@ -14,7 +14,7 @@
<string name="manage_accounts">Behandle kontoer</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_crash_reports">Krasjrapporter</string>
<string name="notification_channel_debugging">Avlusing</string>
<string name="notification_channel_general">Andre viktige meldinger</string>
<string name="notification_channel_sync">Synkronisering</string>
@ -25,7 +25,7 @@
<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_title">EteSync har krasjet!</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>
@ -126,9 +126,9 @@
<string name="collection_members_title">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">Inviter bruker</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="collection_members_adding">Inviterer brukre</string>
<string name="trust_fingerprint_title">Kontroller sikkerhetsfingeravtrykk</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</string>
@ -179,7 +179,7 @@
<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">* Dobbeltsjekk passordet, siden det ikke kan gjenopprettes hvis det er feil!</string>
<string name="login_encryption_check_password">* Husk passordet ditt, det kan ikke gjenopprettes hvis du mister det!</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>
@ -279,7 +279,7 @@
<string name="delete_collection_deleting_collection">Sletter samling</string>
<!-- JournalViewer -->
<string name="journal_entries_list_empty">Samlingen er tom.
\n(Kanskje den fortsatt synkroniserer\?)</string>
\nKanskje den fortsatt synkroniserer\?</string>
<string name="journal_entries_loading">Laster endringsloggsoppføringer …</string>
<!-- ExceptionInfoFragment -->
<string name="exception">En feil har oppstått.</string>
@ -288,7 +288,7 @@
<string name="exception_show_details">Vis detaljer</string>
<!-- sync errors and DebugInfoActivity -->
<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="debug_info_more_data_shared">Når du klikker på «Del» vil dataene under bli sendt til utviklerne, i tillegg til noe ekstra feilsporingsinfo, vedlagt. Merk at forsendelsen kan inneholde sensitiv info.</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>
@ -365,7 +365,7 @@
<string name="sync_error_generic">Kunne ikke synkronisere (%s)</string>
<string name="signup_password_restrictions">Passordet må være minst 8 tegn langt</string>
<string name="signup_title">Skriv inn innloggingsdetaljer</string>
<string name="login_username_error">Et gyldig brukernavn kreves</string>
<string name="login_username_error">Brukernavnet må være minst 6 tegn, og bestå av kun bokstaver, tall og ./-/_.</string>
<string name="login_username">Bruikernavn</string>
<string name="journal_item_tab_revisions">Revisjoner</string>
<string name="invitations_reject">Avslå</string>
@ -378,4 +378,21 @@
<string name="collection_members_no_access">Kun administratorer kan håndtere samlingsmedlemskap. Ønsker du å forlate denne samlingen\?</string>
<string name="collection_members_remove_admin">Fjerning av tilganger for administratorer støttes ikke.</string>
<string name="edit_owner_only_anon">Kun eiere av denne samlingen kan redigere den.</string>
</resources>
<string name="collection_members_add_success">Invitasjon sendt. Brukeren vil bli lagt til når invitasjonen har blitt godkjent.</string>
<string name="collection_members_add_read_only">Skrivebeskyttet</string>
<string name="account_wizard_collections_body">For å kunne bruke EteSync må du opprette samlinger å lagre dataen din i. Klikk «Opprett» for å opprette en forvalgt kalender, adressebok og gjøremålsliste.</string>
<string name="account_wizard_collections_title">Velkommen til EteSync.</string>
<string name="retry">Prøv igjen</string>
<string name="skip">Hopp over</string>
<string name="create">Opprett</string>
<string name="migrate_v2_wizard_migrate_progress_entries">Migrerer oppføring %d/%d</string>
<string name="migrate_v2_wizard_welcome_body_extra">Forsikre deg om at du har god tilknytning til Internett, og nok batteri, siden dette kan ta sin tid.
\nFor å fortsette, velg hvorvidt du ønsker å registrere en ny EteSync 2.0-konto, eller logge inn med en eksisterende konto.</string>
<string name="migrate_v2_wizard_migrate_progress_done_malformed">Ignorerte %d feilformaterte oppføringer (antagelig trygt å se bort fra).</string>
<string name="migrate_v2_wizard_migrate_progress_done">Kontoen din har blitt migrert.
\nBekreft at alt ble flyttet over på riktig vis, og fjern din gamle konto når du er ferdig.</string>
<string name="migrate_v2_wizard_migrate_progress">Migrerer samling %d/%d</string>
<string name="migrate_v2_wizard_migrate_title">Migrerer …</string>
<string name="migrate_v2_wizard_welcome_body">Dette verktøyet hjelper deg å migrere din data til EteSync 2.0. Migrasjonen sletter ingen data, den kopierer kun data over til den nye EteSync 2.0-tjeneren. Dette betyr at det ikke er muligheter for datatap under migrasjonen.</string>
<string name="migrate_v2_wizard_welcome_title">EteSync 2.0-migrasjon</string>
</resources>

View File

@ -52,8 +52,46 @@
<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="please_wait">Пожалуйста, подождите…</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="address_books_authority_title">Адресные книги</string>
<string name="app_name">EteSync</string>
<string name="accounts_global_sync_enable">Включить</string>
<string name="accounts_global_sync_disabled">Общесистемная автоматическая синхронизация отключена</string>
<string name="account_list_empty">Добро пожаловать в EteSync!</string>
<string name="navigation_drawer_contact">Связаться с нами</string>
<string name="navigation_drawer_report_issue">Сообщить о проблеме</string>
<string name="navigation_drawer_guide">Инструкция пользователя</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_website">Веб-сайт</string>
<string name="navigation_drawer_external_links">Полезные ссылки</string>
<string name="navigation_drawer_settings">Настройки</string>
<string name="navigation_drawer_about">О / Лицензия</string>
<string name="navigation_drawer_subtitle">Адаптер безопасной синхронизации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="logging_couldnt_create_file">Невозможно создать внешний файл лога: %s</string>
<string name="logging_to_external_storage_warning">Выключение удалит логи</string>
<string name="logging_to_external_storage">Логирование на внешнее хранилище: %s</string>
<string name="logging_davdroid_file_logging">Логирование файлов EteSync включено</string>
<string name="about_license_info_no_warranty">Эта программа поставляется СОВЕРШЕННО БЕЗ ГАРАНТИЙ. Это бесплатное программное обеспечение, и вы можете распространять его при определенных условиях.</string>
<string name="startup_vendor_specific_bugs_message">EteSync обнаружил, что вы используете версию Android, которая может содержать ошибки производителя.
\nПожалуйста, посмотрите FAQ для получения дополнительной информации.</string>
<string name="startup_vendor_specific_bugs">Возможные ошибки поставщика</string>
<string name="startup_development_version_message">Это разрабатываемая версия EteSync. Имейте в виду, что все может работать не так, как ожидалось. Пожалуйста, дайте нам конструктивный отзыв для улучшения EteSync.</string>
<string name="startup_battery_optimization_disable">Выключить для EteSync</string>
<string name="startup_battery_optimization_message">Android может отключить / уменьшить синхронизацию EteSync через несколько дней. Чтобы этого не произошло, отключите оптимизацию батареи.</string>
<string name="crash_email_body">Если возможно, укажите любую другую соответствующую информацию, например, что вы сделали, чтобы вызвать эту ошибку.</string>
<string name="crash_email_subject">Отладочная информация EteSync</string>
<string name="crash_message">Пожалуйста, отправьте трассировку стека разработчикам.</string>
<string name="crash_title">EteSync сломался!</string>
<string name="notification_channel_sync_status_desc">Информационные сообщения о состоянии, такие как сводка изменений синхронизации</string>
<string name="notification_channel_sync_status">Статус сообщений</string>
<string name="notification_channel_sync_warnings_desc">Не критические проблемы синхронизации, такие как отклоненные изменения коллекций, доступных только для чтения</string>
<string name="notification_channel_sync_warnings">Предупреждения синхронизации</string>
<string name="notification_channel_sync_errors_desc">Важные ошибки, которые останавливают синхронизацию, например, неожиданные ответы сервера</string>
<string name="notification_channel_crash_reports">Отчеты о падениях</string>
<string name="retry">Повторить</string>
<string name="skip">Пропустить</string>
<string name="create">Создать</string>
<string name="account_title_address_book">Адресная книга EteSync</string>
</resources>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="notification_channel_sync_status_desc">Informačné správy o stave ako napríklad sumár zmien pri synchronizácii</string>
<string name="notification_channel_sync_status">Správy o stave</string>
<string name="notification_channel_sync_warnings">Varovania k synchronizácii</string>
<string name="notification_channel_sync_errors_desc">Dôležité chyby zastavujúce synchronizáciu, ako napríklad neočakávaná odpoveď servera</string>
<string name="notification_channel_sync_errors">Chyby pri synchronizácii</string>
<string name="notification_channel_sync">Synchronizácia</string>
<string name="notification_channel_general">Iné dôležité správy</string>
<string name="notification_channel_debugging">Hľadanie chýb</string>
<string name="retry">Skúsiť znovu</string>
<string name="skip">Preskočiť</string>
<string name="create">Vytvoriť</string>
<string name="send">Odoslať</string>
<string name="please_wait">Čakajte prosím…</string>
<string name="manage_accounts">Spravovať účty</string>
<string name="help">Pomoc</string>
<string name="address_books_authority_title">Zoznamy kontaktov</string>
<string name="account_title_address_book">EteSync Kontakty</string>
<string name="app_name">EteSync</string>
</resources>

View File

@ -176,7 +176,7 @@
<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="debug_info_more_data_shared">Paylaş düğmesine tıklamak, bazı ek hata ayıklama bilgilerinin yanı sıra aşağıdaki verileri geliştiricilere gönderecektir. Lütfen bunun bazı özel bilgiler içerebileceğini unutmayın.</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>
@ -384,4 +384,14 @@
<string name="skip">Atla</string>
<string name="create">Oluştur</string>
<string name="collection_members_add_read_only">Salt okunur</string>
<string name="migrate_v2_wizard_migrate_progress_done">Hesabınız taşındı.
\nLütfen her şeyin doğru bir şekilde kopyalandığını doğrulayın ve işiniz bittiğinde eski hesabınızı kaldırın.</string>
<string name="migrate_v2_wizard_migrate_progress_entries">Taşınan girdiler %d/%d</string>
<string name="migrate_v2_wizard_migrate_progress">Koleksiyon taşınıyor %d/%d</string>
<string name="migrate_v2_wizard_migrate_title">Taşınıyor…</string>
<string name="migrate_v2_wizard_welcome_body_extra">Bu biraz zaman alabileceğinden lütfen iyi bir internet bağlantınız ve yeterli piliniz olduğundan emin olun.
\nDevam etmek için, lütfen aşağıdan yeni bir EteSync 2.0 hesabına kaydolmak veya mevcut bir hesapla oturum açmak seçeneklerinden birini seçin.</string>
<string name="migrate_v2_wizard_welcome_body">Bu araç verilerinizi EteSync 2.0\'a taşımanıza yardımcı olacaktır. Taşıma herhangi bir veriyi silmez. Verilerinizi yalnızca yeni EteSync 2.0 sunucusuna kopyalar. Bu, taşıma esnasında veri kaybı riski olmadığı anlamına gelir.</string>
<string name="migrate_v2_wizard_welcome_title">EteSync 2.0\'a Taşıma</string>
<string name="migrate_v2_wizard_migrate_progress_done_malformed">%d hatalı biçimlendirilmiş girdi yok sayıldı (yok sayılmaları muhtemelen güvenli).</string>
</resources>

View File

@ -1,127 +1,127 @@
<?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="manage_accounts">管理账户</string>
<string name="please_wait">请稍等...</string>
<string name="send">发送</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">电池优化</string>
<string name="startup_dont_show_again">不再显示</string>
<!--AboutActivity-->
<string name="about_license_terms">许可协议</string>
<string name="about_license_info_no_warranty">本程序不附带任何担保。这是一款自由软件,你可以有条件地传播它。</string>
<!--global settings-->
<string name="logging_to_external_storage">记录日志到外部存储 %s</string>
<string name="logging_to_external_storage_warning">请尽快删除日志!</string>
<string name="logging_couldnt_create_file">无法创建外部日志文件 %s</string>
<string name="logging_no_external_storage">找不到外部存储</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">打开导航抽屉</string>
<string name="navigation_drawer_close">关闭导航抽屉</string>
<string name="navigation_drawer_about">关于 / 许可</string>
<string name="navigation_drawer_settings">设置</string>
<string name="navigation_drawer_external_links">外部链接</string>
<string name="navigation_drawer_website">应用网站</string>
<string name="navigation_drawer_faq">常见问题</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">设置</string>
<string name="app_settings_user_interface">用户界面</string>
<string name="app_settings_reset_hints">重设提示</string>
<string name="app_settings_reset_hints_summary">重新显示之前忽略过的提示</string>
<string name="app_settings_reset_hints_success">所有提示将会再次显示</string>
<string name="app_settings_connection">连接</string>
<string name="app_settings_override_proxy">覆盖代理设置</string>
<string name="app_settings_override_proxy_on">使用自定义代理设置</string>
<string name="app_settings_override_proxy_off">使用系统默认代理设置</string>
<string name="app_settings_override_proxy_host">HTTP 代理主机名</string>
<string name="app_settings_override_proxy_port">HTTP 代理端口</string>
<string name="app_settings_security">安全</string>
<string name="app_settings_distrust_system_certs">不信任系统证书</string>
<string name="app_settings_distrust_system_certs_on">系统和用户增加的发布者不会被信任</string>
<string name="app_settings_distrust_system_certs_off">系统和用户增加的发布者会被信任(推荐)</string>
<string name="app_settings_reset_certificates">重设证书信任状态</string>
<string name="app_settings_reset_certificates_summary">重设所有自定义证书的信任状态</string>
<string name="app_settings_reset_certificates_success">所有自定义证书已清除</string>
<string name="app_settings_debug">调试</string>
<string name="app_settings_log_to_external_storage">外部文件日志</string>
<string name="app_settings_log_to_external_storage_on">记录日志到外部存储(如果可用)</string>
<string name="app_settings_log_to_external_storage_off">外部文件日志已禁用</string>
<string name="app_settings_show_debug_info">显示调试信息</string>
<string name="app_settings_show_debug_info_details">查看软件和配置信息</string>
<!--AccountActivity-->
<string name="account_synchronize_now"> 立即同步</string>
<string name="account_synchronizing_now">正在同步</string>
<string name="account_settings">账户设置</string>
<string name="account_delete">删除账户</string>
<string name="account_delete_confirmation_title">真的要删除账户吗?</string>
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">日历权限</string>
<string name="permissions_calendar_request">请求日历权限</string>
<string name="permissions_contacts">通讯录权限</string>
<string name="permissions_contacts_request">请求通讯录权限</string>
<string name="permissions_opentasks">OpenTasks 权限</string>
<string name="permissions_opentasks_request">请求 OpenTasks 权限</string>
<!--AddAccountActivity-->
<string name="login_title">增加账户</string>
<string name="login_email_address">Email 地址</string>
<string name="login_email_address_error">请输入有效 Email 地址</string>
<string name="login_password">密码</string>
<string name="login_password_required">请输入密码</string>
<string name="login_back">返回</string>
<string name="login_configuration_detection">正在配置</string>
<string name="login_querying_server">正在与服务器通信,请稍等...</string>
<string name="login_view_logs">查看日志</string>
<!--AccountSettingsActivity-->
<string name="settings_title">设置:%s</string>
<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>每 1 小时</item>
<item>每 2 小时</item>
<item>每 4 小时</item>
<item>每 24 小时</item>
</string-array>
<string name="settings_sync_wifi_only">只在 WiFi 下同步</string>
<string name="settings_sync_wifi_only_on">同步只在 WiFi 连接下进行</string>
<string name="settings_sync_wifi_only_off">同步不受数据连接类型限制</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID 限制</string>
<string name="settings_sync_wifi_only_ssid_on">同步只在 %s 网络下进行</string>
<string name="settings_sync_wifi_only_ssid_off">任何 WiFi 网络下均会同步</string>
<string name="settings_sync_wifi_only_ssid_message">输入 WiFi 网络的名称 (SSID) ,即可限制同步只在此网络下进行。留空则不限制。</string>
<!--collection management-->
<string name="create_addressbook">创建通讯录</string>
<string name="create_addressbook_display_name_hint">我的通讯录</string>
<string name="create_collection_creating">正在创建集合</string>
<string name="create_collection_display_name">集合显示名称(标题)</string>
<string name="create_collection_display_name_required">请输入标题</string>
<string name="create_collection_description">简介(可选)</string>
<string name="create_collection_create">创建</string>
<string name="delete_collection">删除集合</string>
<string name="delete_collection_confirm_title">你确定吗?</string>
<string name="delete_collection_confirm_warning">这个集合 %s 及其所有数据将会从服务器删除。</string>
<string name="delete_collection_deleting_collection">正在删除集合</string>
<!--ExceptionInfoFragment-->
<string name="exception">出现错误</string>
<string name="exception_httpexception">出现 HTTP 错误</string>
<string name="exception_ioexception">出现 I/O 错误</string>
<string name="exception_show_details">显示详情</string>
<!--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="manage_accounts">管理账户</string>
<string name="please_wait">请稍等</string>
<string name="send">发送</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">电池优化</string>
<string name="startup_dont_show_again">不再显示</string>
<!--AboutActivity-->
<string name="about_license_terms">许可协议</string>
<string name="about_license_info_no_warranty">本程序不附带任何担保。这是一款自由软件,你可以有条件地传播它。</string>
<!--global settings-->
<string name="logging_to_external_storage">记录日志到外部存储 %s</string>
<string name="logging_to_external_storage_warning">禁用会删除日志</string>
<string name="logging_couldnt_create_file">无法创建外部日志文件 %s</string>
<string name="logging_no_external_storage">找不到外部存储</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">打开导航抽屉</string>
<string name="navigation_drawer_close">关闭导航抽屉</string>
<string name="navigation_drawer_about">关于 / 许可</string>
<string name="navigation_drawer_settings">设置</string>
<string name="navigation_drawer_external_links">外部链接</string>
<string name="navigation_drawer_website">应用网站</string>
<string name="navigation_drawer_faq">常见问题</string>
<!--AccountUpdateService-->
<!--AppSettingsActivity-->
<string name="app_settings">设置</string>
<string name="app_settings_user_interface">用户界面</string>
<string name="app_settings_reset_hints">重设提示</string>
<string name="app_settings_reset_hints_summary">重新显示之前忽略过的提示</string>
<string name="app_settings_reset_hints_success">所有提示将会再次显示</string>
<string name="app_settings_connection">连接</string>
<string name="app_settings_override_proxy">覆盖代理设置</string>
<string name="app_settings_override_proxy_on">使用自定义代理设置</string>
<string name="app_settings_override_proxy_off">使用系统默认代理设置</string>
<string name="app_settings_override_proxy_host">HTTP 代理主机名</string>
<string name="app_settings_override_proxy_port">HTTP 代理端口</string>
<string name="app_settings_security">安全</string>
<string name="app_settings_distrust_system_certs">不信任系统证书</string>
<string name="app_settings_distrust_system_certs_on">系统和用户增加的发布者不会被信任</string>
<string name="app_settings_distrust_system_certs_off">系统和用户增加的发布者会被信任(推荐)</string>
<string name="app_settings_reset_certificates">重设证书信任状态</string>
<string name="app_settings_reset_certificates_summary">重设所有自定义证书的信任状态</string>
<string name="app_settings_reset_certificates_success">所有自定义证书已清除</string>
<string name="app_settings_debug">调试</string>
<string name="app_settings_log_to_external_storage">外部文件日志</string>
<string name="app_settings_log_to_external_storage_on">记录日志到外部存储(如果可用)</string>
<string name="app_settings_log_to_external_storage_off">外部文件日志已禁用</string>
<string name="app_settings_show_debug_info">显示调试信息</string>
<string name="app_settings_show_debug_info_details">查看软件和配置信息</string>
<!--AccountActivity-->
<string name="account_synchronize_now"> 立即同步</string>
<string name="account_synchronizing_now">正在同步</string>
<string name="account_settings">账户设置</string>
<string name="account_delete">删除账户</string>
<string name="account_delete_confirmation_title">真的要删除账户吗?</string>
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
<!--PermissionsActivity-->
<string name="permissions_calendar">日历权限</string>
<string name="permissions_calendar_request">请求日历权限</string>
<string name="permissions_contacts">通讯录权限</string>
<string name="permissions_contacts_request">请求通讯录权限</string>
<string name="permissions_opentasks">OpenTasks 权限</string>
<string name="permissions_opentasks_request">请求 OpenTasks 权限</string>
<!--AddAccountActivity-->
<string name="login_title">增加账户</string>
<string name="login_email_address">Email 地址</string>
<string name="login_email_address_error">需要有效的 Email 地址</string>
<string name="login_password">密码</string>
<string name="login_password_required">请输入密码</string>
<string name="login_back">返回</string>
<string name="login_configuration_detection">正在配置</string>
<string name="login_querying_server">正在与服务器通信,请稍等...</string>
<string name="login_view_logs">查看日志</string>
<!--AccountSettingsActivity-->
<string name="settings_title">设置:%s</string>
<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>每 1 小时</item>
<item>每 2 小时</item>
<item>每 4 小时</item>
<item>每 24 小时</item>
</string-array>
<string name="settings_sync_wifi_only">只在 WiFi 下同步</string>
<string name="settings_sync_wifi_only_on">同步只在 WiFi 连接下进行</string>
<string name="settings_sync_wifi_only_off">同步不受数据连接类型限制</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID 限制</string>
<string name="settings_sync_wifi_only_ssid_on">同步只在 %s 网络下进行</string>
<string name="settings_sync_wifi_only_ssid_off">任何 WiFi 网络下均会同步</string>
<string name="settings_sync_wifi_only_ssid_message">输入 WiFi 网络的名称 (SSID) ,即可限制同步只在此网络下进行。留空则不限制。</string>
<!--collection management-->
<string name="create_addressbook">创建通讯录</string>
<string name="create_addressbook_display_name_hint">我的通讯录</string>
<string name="create_collection_creating">正在创建集合</string>
<string name="create_collection_display_name">显示这个集合的名称(标题)</string>
<string name="create_collection_display_name_required">请输入标题</string>
<string name="create_collection_description">描述(可选):</string>
<string name="create_collection_create">创建</string>
<string name="delete_collection">删除集合</string>
<string name="delete_collection_confirm_title">你确定吗?</string>
<string name="delete_collection_confirm_warning">这个集合 %s 及其所有数据将会从服务器删除。</string>
<string name="delete_collection_deleting_collection">正在删除集合</string>
<!--ExceptionInfoFragment-->
<string name="exception">发生错误。</string>
<string name="exception_httpexception">发生 HTTP 错误。</string>
<string name="exception_ioexception">发生 I/O 错误。</string>
<string name="exception_show_details">显示详情</string>
<!--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>

View File

@ -38,8 +38,7 @@
<!-- Crash -->
<string name="crash_title">EteSync crashed!</string>
<string name="crash_message">Please send stack trace to developers.</string>
<string name="crash_email_subject">EteSync Debug Info</string>
<string name="crash_email_body">If possible, please include any other relevant info, such as what you did before this happened.</string>
<string name="crash_email_body">If possible, please include any other relevant info such as what you did before this happened.</string>
<!-- tourguide -->
<string name="tourguide_title">Did you know?</string>
@ -90,6 +89,16 @@
<string name="account_wizard_collections_title">Welcome to EteSync!</string>
<string name="account_wizard_collections_body">In order to start using EteSync you need to create collections to store your data. Click "Create" to create a default calendar, address book and a task list for you.</string>
<!-- Migrate v2 Wizard -->
<string name="migrate_v2_wizard_welcome_title">EteSync 2.0 Migration</string>
<string name="migrate_v2_wizard_welcome_body">This tool will help you migrate your data to EteSync 2.0. The migration doesn\'t delete any data. It only copies your data over to the new EteSync 2.0 server. This means that there is no risk of data-loss in the migration.</string>
<string name="migrate_v2_wizard_welcome_body_extra">Please make sure you have a good internet connection and enough battery as this may take a while.\nTo continue, please choose below whether you would like to signup for a new EteSync 2.0 account, or login using an existing one.</string>
<string name="migrate_v2_wizard_migrate_title">Migrating…</string>
<string name="migrate_v2_wizard_migrate_progress">Migrating collection %d/%d</string>
<string name="migrate_v2_wizard_migrate_progress_entries">Migrated entries %d/%d</string>
<string name="migrate_v2_wizard_migrate_progress_done">Your account has been migrated.\nPlease verify everything was copied over correctly, and remove your old account once done.</string>
<string name="migrate_v2_wizard_migrate_progress_done_malformed">Ignored %d malformed entries (probably safe to ignore).</string>
<!-- AccountUpdateService -->
<!-- AppSettingsActivity -->
@ -368,7 +377,7 @@
<!-- sync errors and DebugInfoActivity -->
<string name="authority_log_provider" translatable="false">com.etesync.syncadapter.log</string>
<string name="debug_info_title">Debug info</string>
<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="debug_info_more_data_shared">Clicking \"Share\" will open your e-mail app with the data below, as well as some additional debug info, attached. It may contain some private info.</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>

View File

@ -33,6 +33,7 @@
<color name="primaryTextColor">#000000</color>
<color name="errorColor">#d32f2f</color>
<color name="infoColor">#E8F4FD</color>
<!-- app theme -->

View File

@ -8,15 +8,15 @@
ext {
kotlin_version = '1.4.10'
gradle_version = '4.0.0'
compileSdkVersion = 28
buildToolsVersion = '29.0.3'
gradle_version = '4.0.1'
compileSdkVersion = 30
buildToolsVersion = '30.0.2'
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.10'
ext.gradle_version = '4.0.0'
ext.gradle_version = '4.0.1'
repositories {
jcenter()

View File

@ -0,0 +1 @@
* EteSync 2.0 support \o/

View File

@ -0,0 +1 @@
* Change the crash reporting to not rely on email (use HTTP instead)

View File

@ -0,0 +1,3 @@
* Debug info: fix manually sending of crash reports to have visual feedback.
* Debug info: fix manually sending of crash reports to include more crash information.
* Fixed a few crashes that were happening in some rare cases.

View File

@ -0,0 +1,2 @@
* Fix crash when generating email invitations while using a French locale
* Uptdate etebase dep to fix issue with custom urls not ending with a slash.

View File

@ -0,0 +1,2 @@
* Fix crashes on older Android devices
* Fix crashes with some screen not loading for some users.

View File

@ -0,0 +1,2 @@
* Event invitations: only send invitations if we are the organizers
* Fix rare crash when pushing changes with EteSync 1.0 accounts

View File

@ -0,0 +1,3 @@
* Improve error handling in sync and import
* Update translations
* Fix some crashes

View File

@ -0,0 +1,5 @@
* Support resizable activities
* Update ical4android dep - should fix issues with duplicate tasks and events
* Update vcard4android dep
* Update gradle and sdk version
* Update translations

View File

@ -0,0 +1 @@
* Fix crash when importing events and also when syncing legacy events

View File

@ -0,0 +1 @@
* Fix "potential vendor bugs" message constantly showing.

View File

@ -0,0 +1 @@
* Fix issues with the Tasks.org integration and subtasks (due to rewriting UIDs).

View File

@ -1,4 +1,4 @@
Secure, end-to-end encrypted, and privacy respecting sync for Android, the desktop and the web. Currently supports calendars, contacts and tasks (using OpenTasks), with more on the way.
Secure, end-to-end encrypted, and privacy respecting sync for your contacts, calendars, and tasks (using Tasks.org and OpenTasks). For notes, please use the EteSync Notes application.
In order to use this application you need to have an account with EteSync (paid hosting), or run your own instance (free and open source). Check out https://www.etesync.com/ for more information.

@ -1 +1 @@
Subproject commit 3429dd1747e076c70535e6e36fb8130b3c718df7
Subproject commit b023c079b2a8cd2fe69360a835cf0d872b71cd53

@ -1 +1 @@
Subproject commit c6d8560a2e958dcdf8b98e3f0c0e8c0a417b0962
Subproject commit e98a3a553511f66b9bbdb914f2d2c91176108aab