Add server type column to caldav accounts

pull/1400/head
Alex Baker 5 years ago
parent 99e624e836
commit e8ad88be21

File diff suppressed because it is too large Load Diff

@ -34,6 +34,7 @@ abstract class CaldavTest : InjectingTestCase() {
preferences.setBoolean(R.string.p_debug_pro, true) preferences.setBoolean(R.string.p_debug_pro, true)
server.start() server.start()
headers.clear()
} }
@After @After
@ -45,14 +46,16 @@ abstract class CaldavTest : InjectingTestCase() {
assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError) assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError)
} }
val headers = HashMap<String, String>()
protected fun enqueue(vararg responses: String) { protected fun enqueue(vararg responses: String) {
responses.forEach { responses.forEach {
server.enqueue( server.enqueue(
MockResponse() MockResponse()
.setResponseCode(207) .setResponseCode(207)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"") .setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.setBody(it) .apply { this@CaldavTest.headers.forEach { (k, v) -> setHeader(k, v) } }
) .setBody(it))
} }
server.enqueue(MockResponse().setResponseCode(500)) server.enqueue(MockResponse().setResponseCode(500))
} }

@ -0,0 +1,99 @@
package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ServerDetectionTest : CaldavTest() {
@Test
fun detectTasksServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3",
accountType = TYPE_TASKS
)
sync()
assertEquals(SERVER_TASKS, loadAccount().serverType)
}
@Test
fun detectNextcloudServer() = runBlocking {
setup("DAV" to NEXTCLOUD_COMPLIANCE)
sync()
assertEquals(SERVER_OWNCLOUD, loadAccount().serverType)
}
@Test
fun detectSabredavServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3"
)
sync()
assertEquals(SERVER_SABREDAV, loadAccount().serverType)
}
@Test
fun detectOpenXchangeServer() = runBlocking {
setup("server" to "Openexchange WebDAV")
sync()
assertEquals(SERVER_OPEN_XCHANGE, loadAccount().serverType)
}
@Test
fun unknownServer() = runBlocking {
setup()
sync()
assertEquals(SERVER_UNKNOWN, loadAccount().serverType)
}
private suspend fun loadAccount(): CaldavAccount =
caldavDao.getAccounts().apply { assertEquals(1, size) }.first()
private suspend fun setup(
vararg headers: Pair<String, String>,
accountType: Int = TYPE_CALDAV
) {
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "username"
password = encryption.encrypt("password")
url = server.url("/remote.php/dav/calendars/user1/").toString()
id = caldavDao.insert(this)
this.accountType = accountType
}
this.headers.putAll(headers)
enqueue(NO_CALENDARS)
}
companion object {
private const val NO_CALENDARS = """<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"/>"""
private const val SABREDAV_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendarserver-subscribed, calendar-auto-schedule, calendar-availability, resource-sharing, calendarserver-sharing"
private const val NEXTCLOUD_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-webcal-cache, calendarserver-subscribed, oc-resource-sharing, oc-calendar-publishing, calendarserver-sharing, nc-calendar-search, nc-enable-birthday-calendar"
}
}

@ -29,7 +29,7 @@ import org.tasks.notifications.NotificationDao
GoogleTaskAccount::class, GoogleTaskAccount::class,
Principal::class, Principal::class,
], ],
version = 78) version = 79)
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao abstract fun notificationDao(): NotificationDao
abstract val tagDataDao: TagDataDao abstract val tagDataDao: TagDataDao

@ -16,6 +16,7 @@ import com.todoroo.astrid.helper.UUIDHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
@ -92,15 +93,18 @@ open class CaldavClient(
.findHomeset() .findHomeset()
} }
suspend fun calendars(): List<Response> = suspend fun calendars(interceptor: Interceptor): List<Response> =
DavResource(httpClient, httpUrl!!) DavResource(
.propfind(1, *calendarProperties) httpClient.newBuilder().addNetworkInterceptor(interceptor).build(),
.filter { (response, relation) -> httpUrl!!
relation == HrefRelation.MEMBER && )
response[ResourceType::class.java]?.types?.contains(CALENDAR) == true && .propfind(1, *calendarProperties)
response[SupportedCalendarComponentSet::class.java]?.supportsTasks == true .filter { (response, relation) ->
} relation == HrefRelation.MEMBER &&
.map { (response, _) -> response } response[ResourceType::class.java]?.types?.contains(CALENDAR) == true &&
response[SupportedCalendarComponentSet::class.java]?.supportsTasks == true
}
.map { (response, _) -> response }
@Throws(IOException::class, HttpException::class) @Throws(IOException::class, HttpException::class)
suspend fun deleteCollection() = withContext(Dispatchers.IO) { suspend fun deleteCollection() = withContext(Dispatchers.IO) {

@ -1,8 +1,12 @@
package org.tasks.caldav package org.tasks.caldav
import android.content.Context import android.content.Context
import at.bitfire.dav4jvm.* import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.PropertyRegistry
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation import at.bitfire.dav4jvm.Response.HrefRelation
import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.exception.HttpException
@ -17,7 +21,9 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.ProdId
import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody import okhttp3.RequestBody
import org.tasks.BuildConfig import org.tasks.BuildConfig
@ -34,6 +40,11 @@ import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
import org.tasks.data.* import org.tasks.data.*
import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
@ -116,10 +127,20 @@ class CaldavSynchronizer @Inject constructor(
} }
} }
@Throws(IOException::class, DavException::class, KeyManagementException::class, NoSuchAlgorithmException::class)
private suspend fun synchronize(account: CaldavAccount) { private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = provider.forAccount(account) val caldavClient = provider.forAccount(account)
val resources = caldavClient.calendars() var serverType = SERVER_UNKNOWN
val resources = caldavClient.calendars(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val response = chain.proceed(chain.request())
serverType = getServerType(account, response.headers)
return response
}
})
if (serverType != account.serverType) {
account.serverType = serverType
caldavDao.update(account)
}
val urls = resources.map { it.href.toString() }.toHashSet() val urls = resources.map { it.href.toString() }.toHashSet()
Timber.d("Found calendars: %s", urls) Timber.d("Found calendars: %s", urls)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, ArrayList(urls))) { for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, ArrayList(urls))) {
@ -170,6 +191,14 @@ class CaldavSynchronizer @Inject constructor(
setError(account, "") setError(account, "")
} }
private fun getServerType(account: CaldavAccount, headers: Headers) = when {
account.isTasksOrg -> SERVER_TASKS
headers["DAV"]?.contains("oc-resource-sharing") == true -> SERVER_OWNCLOUD
headers["x-sabre-version"]?.isNotBlank() == true -> SERVER_SABREDAV
headers["server"] == "Openexchange WebDAV" -> SERVER_OPEN_XCHANGE
else -> SERVER_UNKNOWN
}
private suspend fun setError(account: CaldavAccount, message: String?) { private suspend fun setError(account: CaldavAccount, message: String?) {
account.error = message account.error = message
caldavDao.update(account) caldavDao.update(account)
@ -179,7 +208,6 @@ class CaldavSynchronizer @Inject constructor(
} }
} }
@Throws(DavException::class)
private suspend fun sync( private suspend fun sync(
caldavCalendar: CaldavCalendar, caldavCalendar: CaldavCalendar,
resource: Response, resource: Response,
@ -287,7 +315,6 @@ class CaldavSynchronizer @Inject constructor(
return true return true
} }
@Throws(IOException::class)
private suspend fun pushTask(task: Task, httpClient: OkHttpClient, httpUrl: HttpUrl) { private suspend fun pushTask(task: Task, httpClient: OkHttpClient, httpUrl: HttpUrl) {
Timber.d("pushing %s", task) Timber.d("pushing %s", task)
val caldavTask = caldavDao.getTask(task.id) ?: return val caldavTask = caldavDao.getTask(task.id) ?: return
@ -354,10 +381,10 @@ class CaldavSynchronizer @Inject constructor(
} }
} }
val Response.isOwncloudOwner: Boolean private val Response.isOwncloudOwner: Boolean
get() = this[OCOwnerPrincipal::class.java]?.owner?.let { isCurrentUser(it) } ?: false get() = this[OCOwnerPrincipal::class.java]?.owner?.let { isCurrentUser(it) } ?: false
fun Response.isCurrentUser(href: String) = private fun Response.isCurrentUser(href: String) =
this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true
val Response.principals: List<Principal> val Response.principals: List<Principal>
@ -399,15 +426,15 @@ class CaldavSynchronizer @Inject constructor(
return principals return principals
} }
val Property.Name.toAccess: Int private val Property.Name.toAccess: Int
get() = when(this) { get() = when (this) {
SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER
READ_WRITE, OCAccess.READ_WRITE -> ACCESS_READ_WRITE READ_WRITE, OCAccess.READ_WRITE -> ACCESS_READ_WRITE
READ, OCAccess.READ -> ACCESS_READ_ONLY READ, OCAccess.READ -> ACCESS_READ_ONLY
else -> ACCESS_UNKNOWN else -> ACCESS_UNKNOWN
} }
val Property.Name.toStatus: Int private val Property.Name.toStatus: Int
get() = when (this) { get() = when (this) {
Sharee.INVITE_ACCEPTED, OCUser.INVITE_ACCEPTED -> INVITE_ACCEPTED Sharee.INVITE_ACCEPTED, OCUser.INVITE_ACCEPTED -> INVITE_ACCEPTED
Sharee.INVITE_NORESPONSE, OCUser.INVITE_NORESPONSE -> INVITE_NO_RESPONSE Sharee.INVITE_NORESPONSE, OCUser.INVITE_NORESPONSE -> INVITE_NO_RESPONSE

@ -64,11 +64,14 @@ class CaldavAccount : Parcelable {
var encryptionKey: String? = null var encryptionKey: String? = null
@ColumnInfo(name = "cda_account_type") @ColumnInfo(name = "cda_account_type")
var accountType = 0 var accountType = TYPE_CALDAV
@ColumnInfo(name = "cda_collapsed") @ColumnInfo(name = "cda_collapsed")
var isCollapsed = false var isCollapsed = false
@ColumnInfo(name = "cda_server_type")
var serverType = SERVER_UNKNOWN
constructor() constructor()
@Ignore @Ignore
@ -84,6 +87,7 @@ class CaldavAccount : Parcelable {
accountType = source.readInt() accountType = source.readInt()
encryptionKey = source.readString() encryptionKey = source.readString()
isCollapsed = ParcelCompat.readBoolean(source) isCollapsed = ParcelCompat.readBoolean(source)
serverType = source.readInt()
} }
fun getPassword(encryption: KeyStoreEncryption): String { fun getPassword(encryption: KeyStoreEncryption): String {
@ -143,12 +147,15 @@ class CaldavAccount : Parcelable {
writeInt(accountType) writeInt(accountType)
writeString(encryptionKey) writeString(encryptionKey)
ParcelCompat.writeBoolean(this, isCollapsed) ParcelCompat.writeBoolean(this, isCollapsed)
writeInt(serverType)
} }
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is CaldavAccount) return false if (javaClass != other?.javaClass) return false
other as CaldavAccount
if (id != other.id) return false if (id != other.id) return false
if (uuid != other.uuid) return false if (uuid != other.uuid) return false
@ -161,6 +168,7 @@ class CaldavAccount : Parcelable {
if (encryptionKey != other.encryptionKey) return false if (encryptionKey != other.encryptionKey) return false
if (accountType != other.accountType) return false if (accountType != other.accountType) return false
if (isCollapsed != other.isCollapsed) return false if (isCollapsed != other.isCollapsed) return false
if (serverType != other.serverType) return false
return true return true
} }
@ -177,11 +185,12 @@ class CaldavAccount : Parcelable {
result = 31 * result + (encryptionKey?.hashCode() ?: 0) result = 31 * result + (encryptionKey?.hashCode() ?: 0)
result = 31 * result + accountType result = 31 * result + accountType
result = 31 * result + isCollapsed.hashCode() result = 31 * result + isCollapsed.hashCode()
result = 31 * result + serverType
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=$password, error=$error, isSuppressRepeatingTasks=$isSuppressRepeatingTasks, encryptionKey=$encryptionKey, accountType=$accountType, isCollapsed=$isCollapsed)" return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=$password, error=$error, isSuppressRepeatingTasks=$isSuppressRepeatingTasks, encryptionKey=$encryptionKey, accountType=$accountType, isCollapsed=$isCollapsed, serverType=$serverType)"
} }
fun isTasksSubscription(context: Context): Boolean { fun isTasksSubscription(context: Context): Boolean {
@ -230,6 +239,12 @@ class CaldavAccount : Parcelable {
const val TYPE_TASKS = 4 const val TYPE_TASKS = 4
const val TYPE_ETEBASE = 5 const val TYPE_ETEBASE = 5
const val SERVER_UNKNOWN = -1
const val SERVER_TASKS = 0
const val SERVER_OWNCLOUD = 1
const val SERVER_SABREDAV = 2
const val SERVER_OPEN_XCHANGE = 3
const val ERROR_UNAUTHORIZED = "HTTP ${HttpURLConnection.HTTP_UNAUTHORIZED}" const val ERROR_UNAUTHORIZED = "HTTP ${HttpURLConnection.HTTP_UNAUTHORIZED}"
const val ERROR_PAYMENT_REQUIRED = "HTTP ${HttpURLConnection.HTTP_PAYMENT_REQUIRED}" const val ERROR_PAYMENT_REQUIRED = "HTTP ${HttpURLConnection.HTTP_PAYMENT_REQUIRED}"

@ -4,6 +4,7 @@ import android.database.sqlite.SQLiteException
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.todoroo.astrid.api.FilterListItem.NO_ORDER import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import timber.log.Timber import timber.log.Timber
object Migrations { object Migrations {
@ -374,6 +375,14 @@ object Migrations {
} }
} }
private val MIGRATION_78_79: Migration = object : Migration(78, 79) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE `caldav_accounts` ADD COLUMN `cda_server_type` INTEGER NOT NULL DEFAULT $SERVER_UNKNOWN"
)
}
}
val MIGRATIONS = arrayOf( val MIGRATIONS = arrayOf(
MIGRATION_35_36, MIGRATION_35_36,
MIGRATION_36_37, MIGRATION_36_37,
@ -409,6 +418,7 @@ object Migrations {
MIGRATION_75_76, MIGRATION_75_76,
MIGRATION_76_77, MIGRATION_76_77,
MIGRATION_77_78, MIGRATION_77_78,
MIGRATION_78_79,
) )
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

Loading…
Cancel
Save