mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-22 19:21:09 +00:00
Jtx Board synchronization (#56)
Support tasks and notes synchronization with Jtx Board Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
6ab291bdb2
commit
16311708f8
2
.github/workflows/test-dev.yml
vendored
2
.github/workflows/test-dev.yml
vendored
|
@ -51,7 +51,7 @@ jobs:
|
||||||
- name: Start emulator
|
- name: Start emulator
|
||||||
run: start-emulator.sh
|
run: start-emulator.sh
|
||||||
- name: Run connected tests
|
- name: Run connected tests
|
||||||
run: ./gradlew app:connectedCheck
|
run: ./gradlew app:connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest
|
||||||
- name: Archive results
|
- name: Archive results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
|
|
@ -23,6 +23,7 @@ android {
|
||||||
buildConfigField "String", "userAgent", "\"DAVx5\""
|
buildConfigField "String", "userAgent", "\"DAVx5\""
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
//testInstrumentationRunnerArgument "notAnnotation", "androidx.test.filters.FlakyTest"
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.content.ContentValues
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||||
import android.provider.CalendarContract.Events
|
import android.provider.CalendarContract.Events
|
||||||
|
import androidx.test.filters.FlakyTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import at.bitfire.ical4android.AndroidCalendar
|
import at.bitfire.ical4android.AndroidCalendar
|
||||||
|
@ -113,6 +114,7 @@ class LocalCalendarTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@FlakyTest(detail = "Fails when calendar storage is accessed the first time; probably some initialization thread")
|
||||||
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
||||||
val event = Event().apply {
|
val event = Event().apply {
|
||||||
dtStart = DtStart("20220120T010203Z")
|
dtStart = DtStart("20220120T010203Z")
|
||||||
|
|
|
@ -13,6 +13,7 @@ import android.os.Build
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||||
import android.provider.CalendarContract.Events
|
import android.provider.CalendarContract.Events
|
||||||
|
import androidx.test.filters.FlakyTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import at.bitfire.ical4android.AndroidCalendar
|
import at.bitfire.ical4android.AndroidCalendar
|
||||||
|
@ -66,6 +67,7 @@ class LocalEventTest {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@FlakyTest(detail = "Fails when calendar storage is accessed the first time; probably some initialization thread")
|
||||||
fun testNumDirectInstances_SingleInstance() {
|
fun testNumDirectInstances_SingleInstance() {
|
||||||
val event = Event().apply {
|
val event = Event().apply {
|
||||||
dtStart = DtStart("20220120T010203Z")
|
dtStart = DtStart("20220120T010203Z")
|
||||||
|
@ -306,7 +308,7 @@ class LocalEventTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() {
|
fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -358,6 +360,7 @@ class LocalEventTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@FlakyTest(detail = "Fails when calendar storage is accessed the first time; probably some initialization thread")
|
||||||
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
||||||
val event = Event().apply {
|
val event = Event().apply {
|
||||||
dtStart = DtStart("20220120T010203Z")
|
dtStart = DtStart("20220120T010203Z")
|
||||||
|
|
|
@ -162,6 +162,17 @@
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/sync_calendars"/>
|
android:resource="@xml/sync_calendars"/>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".syncadapter.JtxSyncAdapterService"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter"/>
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/sync_notes"/>
|
||||||
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".syncadapter.OpenTasksSyncAdapterService"
|
android:name=".syncadapter.OpenTasksSyncAdapterService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -265,6 +276,7 @@
|
||||||
<!-- package visiblity – which apps do we need to see? -->
|
<!-- package visiblity – which apps do we need to see? -->
|
||||||
<queries>
|
<queries>
|
||||||
<!-- task providers -->
|
<!-- task providers -->
|
||||||
|
<package android:name="at.techbee.jtx" />
|
||||||
<package android:name="org.dmfs.tasks" />
|
<package android:name="org.dmfs.tasks" />
|
||||||
<package android:name="org.tasks" />
|
<package android:name="org.tasks" />
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ interface CollectionDao: SyncableDao<Collection> {
|
||||||
* - have supportsVEVENT = supportsVTODO = null (= address books)
|
* - have supportsVEVENT = supportsVTODO = null (= address books)
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type " +
|
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type " +
|
||||||
"AND (supportsVTODO OR supportsVEVENT OR (supportsVEVENT IS NULL AND supportsVTODO IS NULL)) ORDER BY displayName, URL")
|
"AND (supportsVTODO OR supportsVEVENT OR supportsVJOURNAL OR (supportsVEVENT IS NULL AND supportsVTODO IS NULL AND supportsVJOURNAL IS NULL)) ORDER BY displayName, URL")
|
||||||
fun pageByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
|
fun pageByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
|
||||||
|
|
||||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync")
|
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync")
|
||||||
|
@ -47,6 +47,9 @@ interface CollectionDao: SyncableDao<Collection> {
|
||||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVEVENT AND sync ORDER BY displayName, url")
|
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVEVENT AND sync ORDER BY displayName, url")
|
||||||
fun getSyncCalendars(serviceId: Long): List<Collection>
|
fun getSyncCalendars(serviceId: Long): List<Collection>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND (supportsVTODO OR supportsVJOURNAL) AND sync ORDER BY displayName, url")
|
||||||
|
fun getSyncJtxCollections(serviceId: Long): List<Collection>
|
||||||
|
|
||||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVTODO AND sync ORDER BY displayName, url")
|
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVTODO AND sync ORDER BY displayName, url")
|
||||||
fun getSyncTaskLists(serviceId: Long): List<Collection>
|
fun getSyncTaskLists(serviceId: Long): List<Collection>
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||||
ContentUris.withAppendedId(
|
ContentUris.withAppendedId(
|
||||||
Events.CONTENT_URI,
|
Events.CONTENT_URI,
|
||||||
eventID
|
eventID
|
||||||
).asSyncAdapter(account),
|
),
|
||||||
arrayOf(Events.DTSTART, Events.LAST_DATE), null, null, null
|
arrayOf(Events.DTSTART, Events.LAST_DATE), null, null, null
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
cursor.moveToNext()
|
cursor.moveToNext()
|
||||||
|
@ -110,7 +110,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||||
|
|
||||||
// add the number of instances of every main event's exception
|
// add the number of instances of every main event's exception
|
||||||
provider.query(
|
provider.query(
|
||||||
Events.CONTENT_URI.asSyncAdapter(account),
|
Events.CONTENT_URI,
|
||||||
arrayOf(Events._ID),
|
arrayOf(Events._ID),
|
||||||
"${Events.ORIGINAL_ID}=?", // get exception events of the main event
|
"${Events.ORIGINAL_ID}=?", // get exception events of the main event
|
||||||
arrayOf("$eventID"), null
|
arrayOf("$eventID"), null
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||||
|
**************************************************************************************************/
|
||||||
|
|
||||||
|
package at.bitfire.davdroid.resource
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.content.ContentProviderClient
|
||||||
|
import android.content.ContentValues
|
||||||
|
import at.bitfire.davdroid.DavUtils
|
||||||
|
import at.bitfire.davdroid.model.Collection
|
||||||
|
import at.bitfire.davdroid.model.SyncState
|
||||||
|
import at.bitfire.ical4android.JtxCollection
|
||||||
|
import at.bitfire.ical4android.JtxCollectionFactory
|
||||||
|
import at.bitfire.ical4android.JtxICalObject
|
||||||
|
import at.techbee.jtx.JtxContract
|
||||||
|
|
||||||
|
class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Long):
|
||||||
|
JtxCollection<JtxICalObject>(account, client, LocalJtxICalObject.Factory, id),
|
||||||
|
LocalCollection<LocalJtxICalObject>{
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(account: Account, client: ContentProviderClient, info: Collection) {
|
||||||
|
val values = valuesFromCollection(info, account)
|
||||||
|
create(account, client, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun valuesFromCollection(info: Collection, account: Account) =
|
||||||
|
ContentValues().apply {
|
||||||
|
put(JtxContract.JtxCollection.URL, info.url.toString())
|
||||||
|
put(JtxContract.JtxCollection.DISPLAYNAME, info.displayName ?: DavUtils.lastSegmentOfUrl(info.url))
|
||||||
|
put(JtxContract.JtxCollection.DESCRIPTION, info.description)
|
||||||
|
put(JtxContract.JtxCollection.OWNER, info.owner?.toString())
|
||||||
|
put(JtxContract.JtxCollection.COLOR, info.color)
|
||||||
|
put(JtxContract.JtxCollection.SUPPORTSVEVENT, info.supportsVEVENT)
|
||||||
|
put(JtxContract.JtxCollection.SUPPORTSVJOURNAL, info.supportsVJOURNAL)
|
||||||
|
put(JtxContract.JtxCollection.SUPPORTSVTODO, info.supportsVTODO)
|
||||||
|
put(JtxContract.JtxCollection.ACCOUNT_NAME, account.name)
|
||||||
|
put(JtxContract.JtxCollection.ACCOUNT_TYPE, account.type)
|
||||||
|
put(JtxContract.JtxCollection.READONLY, info.forceReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override val tag: String
|
||||||
|
get() = "jtx-${account.name}-$id"
|
||||||
|
override val title: String
|
||||||
|
get() = displayname ?: id.toString()
|
||||||
|
override var lastSyncState: SyncState?
|
||||||
|
get() = SyncState.fromString(syncstate)
|
||||||
|
set(value) { syncstate = value.toString() }
|
||||||
|
|
||||||
|
fun updateCollection(info: Collection) {
|
||||||
|
val values = valuesFromCollection(info, account)
|
||||||
|
update(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findDeleted(): List<LocalJtxICalObject> {
|
||||||
|
val values = queryDeletedICalObjects()
|
||||||
|
val localJtxICalObjects = mutableListOf<LocalJtxICalObject>()
|
||||||
|
values.forEach {
|
||||||
|
localJtxICalObjects.add(LocalJtxICalObject.Factory.fromProvider(this, it))
|
||||||
|
}
|
||||||
|
return localJtxICalObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findDirty(): List<LocalJtxICalObject> {
|
||||||
|
val values = queryDirtyICalObjects()
|
||||||
|
val localJtxICalObjects = mutableListOf<LocalJtxICalObject>()
|
||||||
|
values.forEach {
|
||||||
|
localJtxICalObjects.add(LocalJtxICalObject.Factory.fromProvider(this, it))
|
||||||
|
}
|
||||||
|
return localJtxICalObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findByName(name: String): LocalJtxICalObject? {
|
||||||
|
val values = queryByFilename(name) ?: return null
|
||||||
|
return LocalJtxICalObject.Factory.fromProvider(this, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun markNotDirty(flags: Int)= updateSetFlags(flags)
|
||||||
|
|
||||||
|
override fun removeNotDirtyMarked(flags: Int) = deleteByFlags(flags)
|
||||||
|
|
||||||
|
override fun forgetETags() = updateSetETag(null)
|
||||||
|
|
||||||
|
|
||||||
|
object Factory: JtxCollectionFactory<LocalJtxCollection> {
|
||||||
|
override fun newInstance(account: Account, client: ContentProviderClient, id: Long) = LocalJtxCollection(account, client, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||||
|
**************************************************************************************************/
|
||||||
|
|
||||||
|
package at.bitfire.davdroid.resource
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import at.bitfire.ical4android.*
|
||||||
|
import at.techbee.jtx.JtxContract
|
||||||
|
|
||||||
|
class LocalJtxICalObject(
|
||||||
|
collection: JtxCollection<*>,
|
||||||
|
fileName: String?,
|
||||||
|
eTag: String?,
|
||||||
|
scheduleTag: String?,
|
||||||
|
flags: Int
|
||||||
|
) :
|
||||||
|
JtxICalObject(collection),
|
||||||
|
LocalResource<JtxICalObject> {
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.fileName = fileName
|
||||||
|
this.eTag = eTag
|
||||||
|
this.flags = flags
|
||||||
|
this.scheduleTag = scheduleTag
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object Factory : JtxICalObjectFactory<LocalJtxICalObject> {
|
||||||
|
|
||||||
|
override fun fromProvider(
|
||||||
|
collection: JtxCollection<JtxICalObject>,
|
||||||
|
values: ContentValues
|
||||||
|
): LocalJtxICalObject {
|
||||||
|
val fileName = values.getAsString(JtxContract.JtxICalObject.FILENAME)
|
||||||
|
val eTag = values.getAsString(JtxContract.JtxICalObject.ETAG)
|
||||||
|
val scheduleTag = values.getAsString(JtxContract.JtxICalObject.SCHEDULETAG)
|
||||||
|
val flags = values.getAsInteger(JtxContract.JtxICalObject.FLAGS)?: 0
|
||||||
|
|
||||||
|
val localJtxICalObject = LocalJtxICalObject(collection, fileName, eTag, scheduleTag, flags)
|
||||||
|
localJtxICalObject.populateFromContentValues(values)
|
||||||
|
|
||||||
|
return localJtxICalObject
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||||
|
**************************************************************************************************/
|
||||||
|
|
||||||
|
package at.bitfire.davdroid.syncadapter
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.ContentProviderClient
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SyncResult
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import at.bitfire.davdroid.log.Logger
|
||||||
|
import at.bitfire.davdroid.model.AppDatabase
|
||||||
|
import at.bitfire.davdroid.model.Collection
|
||||||
|
import at.bitfire.davdroid.model.Service
|
||||||
|
import at.bitfire.davdroid.resource.LocalJtxCollection
|
||||||
|
import at.bitfire.davdroid.settings.AccountSettings
|
||||||
|
import at.bitfire.ical4android.JtxCollection
|
||||||
|
import at.bitfire.ical4android.TaskProvider
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
class JtxSyncAdapterService: SyncAdapterService() {
|
||||||
|
|
||||||
|
override fun syncAdapter() = JtxSyncAdapter(this)
|
||||||
|
|
||||||
|
|
||||||
|
class JtxSyncAdapter(context: Context): SyncAdapter(context) {
|
||||||
|
|
||||||
|
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||||
|
val accountSettings = AccountSettings(context, account)
|
||||||
|
|
||||||
|
// make sure account can be seen by task provider
|
||||||
|
if (Build.VERSION.SDK_INT >= 26)
|
||||||
|
AccountManager.get(context).setAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName, AccountManager.VISIBILITY_VISIBLE)
|
||||||
|
|
||||||
|
//sync list of collections
|
||||||
|
updateLocalCollections(account, provider)
|
||||||
|
|
||||||
|
// sync contents of collections
|
||||||
|
val collections = JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null)
|
||||||
|
for (collection in collections) {
|
||||||
|
Logger.log.info("Synchronizing $collection")
|
||||||
|
JtxSyncManager(context, account, accountSettings, extras, authority, syncResult, collection).use {
|
||||||
|
it.performSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLocalCollections(account: Account, client: ContentProviderClient) {
|
||||||
|
val db = AppDatabase.getInstance(context)
|
||||||
|
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||||
|
|
||||||
|
val remoteCollections = mutableMapOf<HttpUrl, Collection>()
|
||||||
|
if (service != null)
|
||||||
|
for (collection in db.collectionDao().getSyncJtxCollections(service.id))
|
||||||
|
remoteCollections[collection.url] = collection
|
||||||
|
|
||||||
|
for (jtxCollection in JtxCollection.find(account, client, context, LocalJtxCollection.Factory, null, null))
|
||||||
|
jtxCollection.url?.let { strUrl ->
|
||||||
|
val url = strUrl.toHttpUrl()
|
||||||
|
val info = remoteCollections[url]
|
||||||
|
if (info == null) {
|
||||||
|
Logger.log.fine("Deleting obsolete local collection $url")
|
||||||
|
jtxCollection.delete()
|
||||||
|
} else {
|
||||||
|
// remote CollectionInfo found for this local collection, update data
|
||||||
|
Logger.log.log(Level.FINE, "Updating local collection $url", info)
|
||||||
|
jtxCollection.updateCollection(info)
|
||||||
|
// we already have a local task list for this remote collection, don't take into consideration anymore
|
||||||
|
remoteCollections -= url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new local collections
|
||||||
|
for ((_,info) in remoteCollections) {
|
||||||
|
Logger.log.log(Level.INFO, "Adding local collections", info)
|
||||||
|
LocalJtxCollection.create(account, client, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||||
|
**************************************************************************************************/
|
||||||
|
|
||||||
|
package at.bitfire.davdroid.syncadapter
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SyncResult
|
||||||
|
import android.os.Bundle
|
||||||
|
import at.bitfire.dav4jvm.DavCalendar
|
||||||
|
import at.bitfire.dav4jvm.DavResponseCallback
|
||||||
|
import at.bitfire.dav4jvm.Response
|
||||||
|
import at.bitfire.dav4jvm.exception.DavException
|
||||||
|
import at.bitfire.dav4jvm.property.*
|
||||||
|
import at.bitfire.davdroid.DavUtils
|
||||||
|
import at.bitfire.davdroid.R
|
||||||
|
import at.bitfire.davdroid.log.Logger
|
||||||
|
import at.bitfire.davdroid.model.SyncState
|
||||||
|
import at.bitfire.davdroid.resource.LocalJtxCollection
|
||||||
|
import at.bitfire.davdroid.resource.LocalJtxICalObject
|
||||||
|
import at.bitfire.davdroid.resource.LocalResource
|
||||||
|
import at.bitfire.davdroid.settings.AccountSettings
|
||||||
|
import at.bitfire.ical4android.InvalidCalendarException
|
||||||
|
import at.bitfire.ical4android.JtxICalObject
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.Reader
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
class JtxSyncManager(
|
||||||
|
context: Context,
|
||||||
|
account: Account,
|
||||||
|
accountSettings: AccountSettings,
|
||||||
|
extras: Bundle,
|
||||||
|
authority: String,
|
||||||
|
syncResult: SyncResult,
|
||||||
|
localCollection: LocalJtxCollection
|
||||||
|
): SyncManager<LocalJtxICalObject, LocalJtxCollection, DavCalendar>(context, account, accountSettings, extras, authority, syncResult, localCollection) {
|
||||||
|
|
||||||
|
override fun prepare(): Boolean {
|
||||||
|
collectionURL = (localCollection.url ?: return false).toHttpUrlOrNull() ?: return false
|
||||||
|
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryCapabilities(): SyncState? =
|
||||||
|
remoteExceptionContext {
|
||||||
|
var syncState: SyncState? = null
|
||||||
|
it.propfind(0, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
|
||||||
|
if (relation == Response.HrefRelation.SELF) {
|
||||||
|
response[SupportedReportSet::class.java]?.let { supported ->
|
||||||
|
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
|
||||||
|
}
|
||||||
|
syncState = syncState(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
|
||||||
|
syncState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateUpload(resource: LocalJtxICalObject): RequestBody = localExceptionContext(resource) {
|
||||||
|
Logger.log.log(Level.FINE, "Preparing upload of icalobject ${resource.fileName}", resource)
|
||||||
|
|
||||||
|
val os = ByteArrayOutputStream()
|
||||||
|
resource.write(os)
|
||||||
|
|
||||||
|
os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
|
||||||
|
|
||||||
|
override fun listAllRemote(callback: DavResponseCallback) {
|
||||||
|
remoteExceptionContext { remote ->
|
||||||
|
Logger.log.info("Querying tasks")
|
||||||
|
remote.calendarQuery("VTODO", null, null, callback)
|
||||||
|
Logger.log.info("Querying journals")
|
||||||
|
remote.calendarQuery("VJOURNAL", null, null, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun downloadRemote(bunch: List<HttpUrl>) {
|
||||||
|
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
|
||||||
|
// multiple iCalendars, use calendar-multi-get
|
||||||
|
remoteExceptionContext {
|
||||||
|
it.multiget(bunch) { response, _ ->
|
||||||
|
responseExceptionContext(response) {
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
Logger.log.warning("Received non-successful multiget response for ${response.href}")
|
||||||
|
return@responseExceptionContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val eTag = response[GetETag::class.java]?.eTag
|
||||||
|
?: throw DavException("Received multi-get response without ETag")
|
||||||
|
|
||||||
|
val calendarData = response[CalendarData::class.java]
|
||||||
|
val iCal = calendarData?.iCalendar
|
||||||
|
?: throw DavException("Received multi-get response without address data")
|
||||||
|
|
||||||
|
processICalObject(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postProcess() {
|
||||||
|
/* related-to entries must be updated. The linkedICalObjectId is set to 0 for synced entries as we cannot be sure that the linked entry is already
|
||||||
|
there when the related-to entry is written. therefore we have to update it here in the postProcess() method. */
|
||||||
|
|
||||||
|
localCollection.updateRelatedTo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyInvalidResourceTitle(): String =
|
||||||
|
context.getString(R.string.sync_invalid_event)
|
||||||
|
|
||||||
|
|
||||||
|
private fun processICalObject(fileName: String, eTag: String, reader: Reader) {
|
||||||
|
val icalobjects: MutableList<JtxICalObject> = mutableListOf()
|
||||||
|
try {
|
||||||
|
// parse the reader content and return the list of ICalObjects
|
||||||
|
icalobjects.addAll(JtxICalObject.fromReader(reader, localCollection))
|
||||||
|
} catch (e: InvalidCalendarException) {
|
||||||
|
Logger.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
|
||||||
|
notifyInvalidResource(e, fileName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icalobjects.size == 1) {
|
||||||
|
val newData = icalobjects.first()
|
||||||
|
|
||||||
|
// update local task, if it exists
|
||||||
|
localExceptionContext(localCollection.findByName(fileName)) { local ->
|
||||||
|
if (local != null) {
|
||||||
|
Logger.log.log(Level.INFO, "Updating $fileName in local task list", newData)
|
||||||
|
local.eTag = eTag
|
||||||
|
local.update(newData)
|
||||||
|
syncResult.stats.numUpdates++
|
||||||
|
} else {
|
||||||
|
Logger.log.log(Level.INFO, "Adding $fileName to local task list", newData)
|
||||||
|
|
||||||
|
localExceptionContext(LocalJtxICalObject(localCollection, fileName, eTag, null, LocalResource.FLAG_REMOTELY_PRESENT)) {
|
||||||
|
it.applyNewData(newData)
|
||||||
|
it.add()
|
||||||
|
}
|
||||||
|
syncResult.stats.numInserts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
Logger.log.info("Received VCALENDAR with not exactly one VTODO or VJOURNAL; ignoring $fileName")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import at.bitfire.davdroid.settings.AccountSettings
|
||||||
import at.bitfire.davdroid.settings.SettingsManager
|
import at.bitfire.davdroid.settings.SettingsManager
|
||||||
import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat
|
import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat
|
||||||
import at.bitfire.ical4android.TaskProvider.ProviderName
|
import at.bitfire.ical4android.TaskProvider.ProviderName
|
||||||
|
import at.techbee.jtx.JtxContract
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -284,6 +285,7 @@ class DebugInfoActivity: AppCompatActivity() {
|
||||||
|
|
||||||
val packageNames = mutableSetOf( // we always want info about these packages:
|
val packageNames = mutableSetOf( // we always want info about these packages:
|
||||||
BuildConfig.APPLICATION_ID, // DAVx5
|
BuildConfig.APPLICATION_ID, // DAVx5
|
||||||
|
ProviderName.JtxBoard.packageName, // jtx Board
|
||||||
ProviderName.OpenTasks.packageName, // OpenTasks
|
ProviderName.OpenTasks.packageName, // OpenTasks
|
||||||
ProviderName.TasksOrg.packageName // tasks.org
|
ProviderName.TasksOrg.packageName // tasks.org
|
||||||
)
|
)
|
||||||
|
@ -603,6 +605,7 @@ class DebugInfoActivity: AppCompatActivity() {
|
||||||
fun mainAccount(context: Context) = listOf(
|
fun mainAccount(context: Context) = listOf(
|
||||||
AccountDumpInfo(context.getString(R.string.address_books_authority), null, null),
|
AccountDumpInfo(context.getString(R.string.address_books_authority), null, null),
|
||||||
AccountDumpInfo(CalendarContract.AUTHORITY, CalendarContract.Events.CONTENT_URI, "event(s)"),
|
AccountDumpInfo(CalendarContract.AUTHORITY, CalendarContract.Events.CONTENT_URI, "event(s)"),
|
||||||
|
AccountDumpInfo(ProviderName.JtxBoard.authority, JtxContract.JtxICalObject.CONTENT_URI, "jtx Board ICalObject(s)"),
|
||||||
AccountDumpInfo(ProviderName.OpenTasks.authority, TaskContract.Tasks.getContentUri(ProviderName.OpenTasks.authority), "OpenTasks task(s)"),
|
AccountDumpInfo(ProviderName.OpenTasks.authority, TaskContract.Tasks.getContentUri(ProviderName.OpenTasks.authority), "OpenTasks task(s)"),
|
||||||
AccountDumpInfo(ProviderName.TasksOrg.authority, TaskContract.Tasks.getContentUri(ProviderName.TasksOrg.authority), "tasks.org task(s)"),
|
AccountDumpInfo(ProviderName.TasksOrg.authority, TaskContract.Tasks.getContentUri(ProviderName.TasksOrg.authority), "tasks.org task(s)"),
|
||||||
AccountDumpInfo(ContactsContract.AUTHORITY, ContactsContract.RawContacts.CONTENT_URI, "wrongly assigned raw contact(s)")
|
AccountDumpInfo(ContactsContract.AUTHORITY, ContactsContract.RawContacts.CONTENT_URI, "wrongly assigned raw contact(s)")
|
||||||
|
|
|
@ -64,6 +64,10 @@ class PermissionsFragment: Fragment() {
|
||||||
if (needTasksOrg == true && model.haveTasksOrgPermissions.value == false)
|
if (needTasksOrg == true && model.haveTasksOrgPermissions.value == false)
|
||||||
requestPermissions(TaskProvider.PERMISSIONS_TASKS_ORG, 0)
|
requestPermissions(TaskProvider.PERMISSIONS_TASKS_ORG, 0)
|
||||||
})
|
})
|
||||||
|
model.needJtxPermissions.observe(viewLifecycleOwner, { needJtx ->
|
||||||
|
if (needJtx == true && model.haveJtxPermissions.value == false)
|
||||||
|
requestPermissions(TaskProvider.PERMISSIONS_JTX, 0)
|
||||||
|
})
|
||||||
model.needAllPermissions.observe(viewLifecycleOwner, { needAll ->
|
model.needAllPermissions.observe(viewLifecycleOwner, { needAll ->
|
||||||
if (needAll && model.haveAllPermissions.value == false) {
|
if (needAll && model.haveAllPermissions.value == false) {
|
||||||
val all = mutableSetOf(*CONTACT_PERMISSIONS, *CALENDAR_PERMISSIONS)
|
val all = mutableSetOf(*CONTACT_PERMISSIONS, *CALENDAR_PERMISSIONS)
|
||||||
|
@ -71,6 +75,8 @@ class PermissionsFragment: Fragment() {
|
||||||
all.addAll(TaskProvider.PERMISSIONS_OPENTASKS)
|
all.addAll(TaskProvider.PERMISSIONS_OPENTASKS)
|
||||||
if (model.haveTasksOrgPermissions.value != null)
|
if (model.haveTasksOrgPermissions.value != null)
|
||||||
all.addAll(TaskProvider.PERMISSIONS_TASKS_ORG)
|
all.addAll(TaskProvider.PERMISSIONS_TASKS_ORG)
|
||||||
|
if (model.haveJtxPermissions.value != null)
|
||||||
|
all.addAll(TaskProvider.PERMISSIONS_JTX)
|
||||||
requestPermissions(all.toTypedArray(), 0)
|
requestPermissions(all.toTypedArray(), 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -107,6 +113,8 @@ class PermissionsFragment: Fragment() {
|
||||||
val needOpenTasksPermissions = MutableLiveData<Boolean>()
|
val needOpenTasksPermissions = MutableLiveData<Boolean>()
|
||||||
val haveTasksOrgPermissions = MutableLiveData<Boolean>()
|
val haveTasksOrgPermissions = MutableLiveData<Boolean>()
|
||||||
val needTasksOrgPermissions = MutableLiveData<Boolean>()
|
val needTasksOrgPermissions = MutableLiveData<Boolean>()
|
||||||
|
val haveJtxPermissions = MutableLiveData<Boolean>()
|
||||||
|
val needJtxPermissions = MutableLiveData<Boolean>()
|
||||||
val tasksWatcher = object: PackageChangedReceiver(app) {
|
val tasksWatcher = object: PackageChangedReceiver(app) {
|
||||||
@MainThread
|
@MainThread
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
@ -167,12 +175,24 @@ class PermissionsFragment: Fragment() {
|
||||||
haveTasksOrgPermissions.value = null
|
haveTasksOrgPermissions.value = null
|
||||||
needTasksOrgPermissions.value = null
|
needTasksOrgPermissions.value = null
|
||||||
}
|
}
|
||||||
|
// jtx Board
|
||||||
|
val jtxAvailable = pm.resolveContentProvider(ProviderName.JtxBoard.authority, 0) != null
|
||||||
|
var jtxPermissions: Boolean? = null
|
||||||
|
if (jtxAvailable) {
|
||||||
|
jtxPermissions = havePermissions(getApplication(), TaskProvider.PERMISSIONS_JTX)
|
||||||
|
haveJtxPermissions.value = jtxPermissions
|
||||||
|
needJtxPermissions.value = jtxPermissions
|
||||||
|
} else {
|
||||||
|
haveJtxPermissions.value = null
|
||||||
|
needJtxPermissions.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// "all permissions" switch
|
// "all permissions" switch
|
||||||
val allPermissions = contactPermissions &&
|
val allPermissions = contactPermissions &&
|
||||||
calendarPermissions &&
|
calendarPermissions &&
|
||||||
(!openTasksAvailable || openTasksPermissions == true) &&
|
(!openTasksAvailable || openTasksPermissions == true) &&
|
||||||
(!tasksOrgAvailable || tasksOrgPermissions == true)
|
(!tasksOrgAvailable || tasksOrgPermissions == true) &&
|
||||||
|
(!jtxAvailable || jtxPermissions == true)
|
||||||
haveAllPermissions.value = allPermissions
|
haveAllPermissions.value = allPermissions
|
||||||
needAllPermissions.value = allPermissions
|
needAllPermissions.value = allPermissions
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,18 @@ class TasksFragment: Fragment() {
|
||||||
model.selectPreferredProvider(ProviderName.TasksOrg)
|
model.selectPreferredProvider(ProviderName.TasksOrg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
model.jtxRequested.observe(viewLifecycleOwner) { shallBeInstalled ->
|
||||||
|
if (shallBeInstalled && model.jtxInstalled.value == false) {
|
||||||
|
model.jtxRequested.value = false
|
||||||
|
installApp(ProviderName.JtxBoard.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.jtxSelected.observe(viewLifecycleOwner) { selected ->
|
||||||
|
if (selected && model.currentProvider.value != ProviderName.JtxBoard)
|
||||||
|
model.selectPreferredProvider(ProviderName.JtxBoard)
|
||||||
|
}
|
||||||
|
|
||||||
binding.infoLeaveUnchecked.text = getString(R.string.intro_leave_unchecked, getString(R.string.app_settings_reset_hints))
|
binding.infoLeaveUnchecked.text = getString(R.string.intro_leave_unchecked, getString(R.string.app_settings_reset_hints))
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -104,6 +116,9 @@ class TasksFragment: Fragment() {
|
||||||
val tasksOrgInstalled = MutableLiveData<Boolean>()
|
val tasksOrgInstalled = MutableLiveData<Boolean>()
|
||||||
val tasksOrgRequested = MutableLiveData<Boolean>()
|
val tasksOrgRequested = MutableLiveData<Boolean>()
|
||||||
val tasksOrgSelected = MutableLiveData<Boolean>()
|
val tasksOrgSelected = MutableLiveData<Boolean>()
|
||||||
|
val jtxInstalled = MutableLiveData<Boolean>()
|
||||||
|
val jtxRequested = MutableLiveData<Boolean>()
|
||||||
|
val jtxSelected = MutableLiveData<Boolean>()
|
||||||
val tasksWatcher = object: PackageChangedReceiver(app) {
|
val tasksWatcher = object: PackageChangedReceiver(app) {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
checkInstalled()
|
checkInstalled()
|
||||||
|
@ -146,6 +161,11 @@ class TasksFragment: Fragment() {
|
||||||
tasksOrgInstalled.postValue(tasksOrg)
|
tasksOrgInstalled.postValue(tasksOrg)
|
||||||
tasksOrgRequested.postValue(tasksOrg)
|
tasksOrgRequested.postValue(tasksOrg)
|
||||||
tasksOrgSelected.postValue(taskProvider == ProviderName.TasksOrg)
|
tasksOrgSelected.postValue(taskProvider == ProviderName.TasksOrg)
|
||||||
|
|
||||||
|
val jtxBoard = isInstalled(ProviderName.JtxBoard.packageName)
|
||||||
|
jtxInstalled.postValue(jtxBoard)
|
||||||
|
jtxRequested.postValue(jtxBoard)
|
||||||
|
jtxSelected.postValue(taskProvider == ProviderName.JtxBoard)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isInstalled(packageName: String): Boolean =
|
private fun isInstalled(packageName: String): Boolean =
|
||||||
|
|
|
@ -80,6 +80,7 @@ class CalendarsFragment: CollectionsFragment() {
|
||||||
binding.readOnly.visibility = if (item.readOnly()) View.VISIBLE else View.GONE
|
binding.readOnly.visibility = if (item.readOnly()) View.VISIBLE else View.GONE
|
||||||
binding.events.visibility = if (item.supportsVEVENT == true) View.VISIBLE else View.GONE
|
binding.events.visibility = if (item.supportsVEVENT == true) View.VISIBLE else View.GONE
|
||||||
binding.tasks.visibility = if (item.supportsVTODO == true) View.VISIBLE else View.GONE
|
binding.tasks.visibility = if (item.supportsVTODO == true) View.VISIBLE else View.GONE
|
||||||
|
binding.journals.visibility = if (item.supportsVJOURNAL == true) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
accountModel.toggleSync(item)
|
accountModel.toggleSync(item)
|
||||||
|
|
|
@ -167,7 +167,7 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
|
||||||
} else
|
} else
|
||||||
model.typeError.value = null
|
model.typeError.value = null
|
||||||
|
|
||||||
if (!supportsVEVENT || !supportsVTODO || !supportsVJOURNAL) {
|
if (supportsVEVENT || supportsVTODO || supportsVJOURNAL) {
|
||||||
// only if there's at least one component set not supported; don't include
|
// only if there's at least one component set not supported; don't include
|
||||||
// information about supported components otherwise (means: everything supported)
|
// information about supported components otherwise (means: everything supported)
|
||||||
args.putBoolean(CreateCollectionFragment.ARG_SUPPORTS_VEVENT, supportsVEVENT)
|
args.putBoolean(CreateCollectionFragment.ARG_SUPPORTS_VEVENT, supportsVEVENT)
|
||||||
|
|
10
app/src/main/res/drawable/ic_journals.xml
Normal file
10
app/src/main/res/drawable/ic_journals.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM9,4h2v5l-1,-0.75L9,9L9,4zM18,20L6,20L6,4h1v9l3,-2.25L13,13L13,4h5v16z"/>
|
||||||
|
</vector>
|
|
@ -78,6 +78,13 @@
|
||||||
android:contentDescription="@string/account_task_list"
|
android:contentDescription="@string/account_task_list"
|
||||||
app:srcCompat="@drawable/ic_playlist_add_check"/>
|
app:srcCompat="@drawable/ic_playlist_add_check"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/journals"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/account_journal"
|
||||||
|
app:srcCompat="@drawable/ic_journals"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/action_overflow"
|
android:id="@+id/action_overflow"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -287,6 +287,42 @@
|
||||||
android:clickable="@{!model.haveTasksOrgPermissions}"
|
android:clickable="@{!model.haveTasksOrgPermissions}"
|
||||||
android:checked="@={model.needTasksOrgPermissions}" />
|
android:checked="@={model.needTasksOrgPermissions}" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/jtxHeading"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/card_margin_title_text"
|
||||||
|
android:text="@string/permissions_jtx_title"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="@{model.haveJtxPermissions != null ? View.VISIBLE : View.GONE}"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/jtxStatus"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/jtxSwitch"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/start"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tasksOrgStatus" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/jtxStatus"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@{model.haveJtxPermissions != null ? (model.haveJtxPermissions ? @string/permissions_jtx_status_on : @string/permissions_jtx_status_off) : @string/permissions_jtx_status_not_installed}"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="@{model.haveJtxPermissions != null ? View.VISIBLE : View.GONE}"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/jtxSwitch"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/start"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/jtxHeading" />
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/jtxSwitch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/jtxHeading"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/jtxStatus"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/end"
|
||||||
|
android:visibility="@{model.haveJtxPermissions != null ? View.VISIBLE : View.GONE}"
|
||||||
|
android:clickable="@{!model.haveJtxPermissions}"
|
||||||
|
android:checked="@={model.needJtxPermissions}" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/appSettingsHint"
|
android:id="@+id/appSettingsHint"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
@ -297,7 +333,7 @@
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintEnd_toEndOf="@id/end"
|
app:layout_constraintEnd_toEndOf="@id/end"
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tasksOrgSwitch" />
|
app:layout_constraintTop_toBottomOf="@id/jtxSwitch" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/appSettings"
|
android:id="@+id/appSettings"
|
||||||
|
|
|
@ -92,7 +92,6 @@
|
||||||
android:clickable="@{model.openTasksInstalled}"
|
android:clickable="@{model.openTasksInstalled}"
|
||||||
android:text="@string/intro_tasks_opentasks"
|
android:text="@string/intro_tasks_opentasks"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toTopOf="@id/tasksOrgRadio"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/openTasksSwitch"
|
app:layout_constraintEnd_toStartOf="@id/openTasksSwitch"
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text1" />
|
app:layout_constraintTop_toBottomOf="@id/text1" />
|
||||||
|
@ -101,7 +100,6 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/openTasksRadio"
|
app:layout_constraintTop_toBottomOf="@id/openTasksRadio"
|
||||||
app:layout_constraintBottom_toTopOf="@id/tasksOrgRadio"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintEnd_toStartOf="@id/end"
|
app:layout_constraintEnd_toStartOf="@id/end"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
@ -127,7 +125,6 @@
|
||||||
android:clickable="@{model.tasksOrgInstalled}"
|
android:clickable="@{model.tasksOrgInstalled}"
|
||||||
android:text="@string/intro_tasks_tasks_org"
|
android:text="@string/intro_tasks_tasks_org"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toTopOf="@id/tasksOrgInfo"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/tasksOrgSwitch"
|
app:layout_constraintEnd_toStartOf="@id/tasksOrgSwitch"
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintTop_toBottomOf="@id/openTasksInfo" />
|
app:layout_constraintTop_toBottomOf="@id/openTasksInfo" />
|
||||||
|
@ -136,7 +133,6 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tasksOrgRadio"
|
app:layout_constraintTop_toBottomOf="@id/tasksOrgRadio"
|
||||||
app:layout_constraintBottom_toTopOf="@id/dontShow"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintEnd_toStartOf="@id/end"
|
app:layout_constraintEnd_toStartOf="@id/end"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
@ -152,6 +148,39 @@
|
||||||
app:layout_constraintStart_toEndOf="@id/tasksOrgRadio"
|
app:layout_constraintStart_toEndOf="@id/tasksOrgRadio"
|
||||||
app:layout_constraintEnd_toEndOf="@id/end"/>
|
app:layout_constraintEnd_toEndOf="@id/end"/>
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/jtxRadio"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/card_margin_title_text"
|
||||||
|
android:checked="@={model.jtxSelected}"
|
||||||
|
android:clickable="@{model.jtxInstalled}"
|
||||||
|
android:text="@string/intro_tasks_jtx"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/jtxSwitch"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tasksOrgInfo" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/jtxInfo"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/jtxRadio"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/end"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
app:html="@{@string/intro_tasks_jtx_info}" />
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/jtxSwitch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="@={model.jtxRequested}"
|
||||||
|
android:clickable="@{!model.jtxInstalled}"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/jtxRadio"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/jtxRadio"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/jtxRadio"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/end"/>
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/dontShow"
|
android:id="@+id/dontShow"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -163,7 +192,7 @@
|
||||||
android:visibility="@{model.openTasksInstalled ? View.GONE : View.VISIBLE}"
|
android:visibility="@{model.openTasksInstalled ? View.GONE : View.VISIBLE}"
|
||||||
app:layout_constraintEnd_toEndOf="@id/end"
|
app:layout_constraintEnd_toEndOf="@id/end"
|
||||||
app:layout_constraintStart_toStartOf="@id/start"
|
app:layout_constraintStart_toStartOf="@id/start"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tasksOrgInfo" />
|
app:layout_constraintTop_toBottomOf="@id/jtxInfo" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
<string name="intro_autostart_dont_show">I have done the required settings. Don\'t remind me anymore.*</string>
|
<string name="intro_autostart_dont_show">I have done the required settings. Don\'t remind me anymore.*</string>
|
||||||
<string name="intro_leave_unchecked">* Leave unchecked to be reminded later. Can be reset in app settings / %s.</string>
|
<string name="intro_leave_unchecked">* Leave unchecked to be reminded later. Can be reset in app settings / %s.</string>
|
||||||
<string name="intro_more_info">More information</string>
|
<string name="intro_more_info">More information</string>
|
||||||
|
<string name="intro_tasks_jtx">jtx Board</string>
|
||||||
|
<string name="intro_tasks_jtx_info"><![CDATA[Supports sync of Tasks, Journals and Notes.]]></string>
|
||||||
<string name="intro_tasks_title">Tasks support</string>
|
<string name="intro_tasks_title">Tasks support</string>
|
||||||
<string name="intro_tasks_text1">If tasks are supported by your server, they can be synchronized with a supported tasks app:</string>
|
<string name="intro_tasks_text1">If tasks are supported by your server, they can be synchronized with a supported tasks app:</string>
|
||||||
<string name="intro_tasks_opentasks">OpenTasks</string>
|
<string name="intro_tasks_opentasks">OpenTasks</string>
|
||||||
|
@ -68,10 +70,14 @@
|
||||||
<string name="permissions_calendar_title">Calendar permissions</string>
|
<string name="permissions_calendar_title">Calendar permissions</string>
|
||||||
<string name="permissions_calendar_status_off">No calendar sync (not recommended)</string>
|
<string name="permissions_calendar_status_off">No calendar sync (not recommended)</string>
|
||||||
<string name="permissions_calendar_status_on">Calendar sync possible</string>
|
<string name="permissions_calendar_status_on">Calendar sync possible</string>
|
||||||
|
<string name="permissions_jtx_title">jtx Board permissions</string>
|
||||||
|
<string name="permissions_jtx_status_not_installed">No task, journals & notes sync (not installed)</string>
|
||||||
|
<string name="permissions_jtx_status_off">No tasks, journals, notes sync</string>
|
||||||
|
<string name="permissions_jtx_status_on">Tasks, journals, notes sync possible</string>
|
||||||
<string name="permissions_opentasks_title">OpenTasks permissions</string>
|
<string name="permissions_opentasks_title">OpenTasks permissions</string>
|
||||||
<string name="permissions_tasksorg_title">Tasks permissions</string>
|
<string name="permissions_tasksorg_title">Tasks permissions</string>
|
||||||
<string name="permissions_tasks_status_not_installed">No task sync (not installed)</string>
|
<string name="permissions_tasks_status_not_installed">No task sync (not installed)</string>
|
||||||
<string name="permissions_tasks_status_off">No task sync (not recommended)</string>
|
<string name="permissions_tasks_status_off">No task sync</string>
|
||||||
<string name="permissions_tasks_status_on">Task sync possible</string>
|
<string name="permissions_tasks_status_on">Task sync possible</string>
|
||||||
<string name="permissions_autoreset_title">Keep permissions</string>
|
<string name="permissions_autoreset_title">Keep permissions</string>
|
||||||
<string name="permissions_autoreset_status_off">Permissions may be reset automatically (not recommended)</string>
|
<string name="permissions_autoreset_status_off">Permissions may be reset automatically (not recommended)</string>
|
||||||
|
@ -227,6 +233,7 @@
|
||||||
<string name="account_read_only">read-only</string>
|
<string name="account_read_only">read-only</string>
|
||||||
<string name="account_calendar">calendar</string>
|
<string name="account_calendar">calendar</string>
|
||||||
<string name="account_task_list">task list</string>
|
<string name="account_task_list">task list</string>
|
||||||
|
<string name="account_journal">journal</string>
|
||||||
<string name="account_only_personal">Show only personal</string>
|
<string name="account_only_personal">Show only personal</string>
|
||||||
<string name="account_refresh_address_book_list">Refresh address book list</string>
|
<string name="account_refresh_address_book_list">Refresh address book list</string>
|
||||||
<string name="account_create_new_address_book">Create new address book</string>
|
<string name="account_create_new_address_book">Create new address book</string>
|
||||||
|
|
5
app/src/main/res/xml/sync_notes.xml
Normal file
5
app/src/main/res/xml/sync_notes.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:accountType="@string/account_type"
|
||||||
|
android:contentAuthority="at.techbee.jtx.provider"
|
||||||
|
android:allowParallelSyncs="true"
|
||||||
|
android:supportsUploading="true" />
|
Loading…
Reference in a new issue