diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..3ece0787 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: etesync +custom: https://www.etesync.com/contribute/#donate diff --git a/ChangeLog.md b/ChangeLog.md index f72302a4..47b84309 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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. diff --git a/app/build.gradle b/app/build.gradle index d45cb316..28566b96 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt index b981122e..345ebb71 100644 --- a/app/proguard-rules.txt +++ b/app/proguard-rules.txt @@ -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. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c4fe99b..0b5f8112 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,6 +52,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" + android:resizeableActivity="true" tools:ignore="UnusedAttribute"> + { @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index b4788739..da43839f 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -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() { diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 8017e033..b9963659 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -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() diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt index b6c9d6a3..88d4dc1d 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -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() } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt index e5206ee5..e0e90f1e 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -83,7 +83,7 @@ class LocalEvent : AndroidEvent, LocalResource { 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 { .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 { 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) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index 6af2801a..eb25a461 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -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) - )) + ) } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index d39d8d4b..15ed97fb 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -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?.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 { 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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt index 67e7a300..b13c085f 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt @@ -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) { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index ad266529..6536314d 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt index 8dc159e9..29d8b4e7 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt @@ -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) { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt index e58f864b..cb99d777 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt @@ -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) { } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt index 63b2273b..62488fc5 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt @@ -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) } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 0722be42..8ff0d4ae 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -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 { val ret = LinkedList() 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) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt index 0c478331..500ca303 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt @@ -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 diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt index 22d18caf..b2c4224b 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt @@ -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) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt index 1953c47f..9853dac8 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AboutActivity.kt @@ -100,7 +100,7 @@ class AboutActivity : BaseActivity() { } override fun onCreateLoader(id: Int, args: Bundle?): Loader { - return LicenseLoader(context!!, args!!.getString(KEY_FILE_NAME)) + return LicenseLoader(requireContext(), args!!.getString(KEY_FILE_NAME)!!) } override fun onLoadFinished(loader: Loader, license: Spanned) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index dbea30be..5b275254 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -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, 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 { - return AccountLoader(this, account) - } - override fun refresh() { - loaderManager.restartLoader(0, intent.extras, this) + model.loadAccount() } - override fun onLoadFinished(loader: Loader, 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) { - 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(context), AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver { + class AccountInfoViewModel : ViewModel(), AccountUpdateService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver { + private val holder = MutableLiveData() + 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 { 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" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt index 893a946b..0cad0e8d 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt @@ -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 { - return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) + return AccountSettingsLoader(requireContext(), (args!!.getParcelable(KEY_ACCOUNT) as Account?)!!) } override fun onLoadFinished(loader: Loader, 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 { - return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) + return AccountSettingsLoader(context!!, (args!!.getParcelable(KEY_ACCOUNT) as? Account)!!) } override fun onLoadFinished(loader: Loader, settings: AccountSettings?) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt index 5371cd1e..6b17693f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.kt @@ -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() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt index d61e4ba9..a1f1768c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt @@ -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) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt index 4705a0c7..33fd9c72 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.kt @@ -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 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") } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt index 2f4a18bc..23e62fcc 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt @@ -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) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/MigrateV2Activity.kt b/app/src/main/java/com/etesync/syncadapter/ui/MigrateV2Activity.kt new file mode 100644 index 00000000..9f40ba82 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/MigrateV2Activity.kt @@ -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