Owners can remove shared list members

Supports Tasks.org, Nextcloud, ownCloud, and sabre/dav
pull/1400/head
Alex Baker 5 years ago
parent e8ad88be21
commit 42f34c1fab

@ -127,7 +127,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
progressView.visibility = View.GONE progressView.visibility = View.GONE
} }
private fun requestInProgress(): Boolean { protected fun requestInProgress(): Boolean {
return progressView.visibility == View.VISIBLE return progressView.visibility == View.VISIBLE
} }

@ -2,8 +2,17 @@ package org.tasks.caldav
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -15,10 +24,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
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.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.Principal import org.tasks.data.Principal
import org.tasks.data.Principal.Companion.name import org.tasks.data.Principal.Companion.name
import org.tasks.data.PrincipalDao import org.tasks.data.PrincipalDao
@ -29,6 +44,8 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
@Inject lateinit var principalDao: PrincipalDao @Inject lateinit var principalDao: PrincipalDao
private val viewModel: CaldavCalendarViewModel by viewModels()
private val createCalendarViewModel: CreateCalendarViewModel by viewModels() private val createCalendarViewModel: CreateCalendarViewModel by viewModels()
private val deleteCalendarViewModel: DeleteCalendarViewModel by viewModels() private val deleteCalendarViewModel: DeleteCalendarViewModel by viewModels()
private val updateCalendarViewModel: UpdateCalendarViewModel by viewModels() private val updateCalendarViewModel: UpdateCalendarViewModel by viewModels()
@ -38,6 +55,14 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.inFlight.observe(this) { progressView.isVisible = it }
viewModel.error.observe(this) { throwable ->
throwable?.let {
requestFailed(it)
viewModel.error.value = null
}
}
createCalendarViewModel.observe(this, this::createSuccessful, this::requestFailed) createCalendarViewModel.observe(this, this::createSuccessful, this::requestFailed)
deleteCalendarViewModel.observe(this, this::onDeleted, this::requestFailed) deleteCalendarViewModel.observe(this, this::onDeleted, this::requestFailed)
updateCalendarViewModel.observe( updateCalendarViewModel.observe(
@ -54,6 +79,29 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
} }
} }
private val canRemovePrincipals: Boolean
get() = caldavCalendar?.access == ACCESS_OWNER && caldavAccount.canRemovePrincipal
private fun onRemove(principal: Principal) {
if (requestInProgress()) {
return
}
dialogBuilder
.newDialog(R.string.remove_user)
.setMessage(R.string.remove_user_confirmation, principal.name, caldavCalendar?.name)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ -> removePrincipal(principal) }
.show()
}
private fun removePrincipal(principal: Principal) = lifecycleScope.launch {
try {
viewModel.remove(caldavAccount, caldavCalendar!!, principal)
} catch (e: Exception) {
requestFailed(e)
}
}
@Composable @Composable
private fun PrincipalList(principals: List<Principal>) { private fun PrincipalList(principals: List<Principal>) {
tasksTheme.TasksTheme { tasksTheme.TasksTheme {
@ -91,6 +139,19 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,
) )
if (canRemovePrincipals) {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { onRemove(principal) }) {
Icon(
painter = painterResource(R.drawable.ic_outline_clear_24px),
contentDescription = null,
tint = colorResource(R.color.icon_tint_with_alpha),
)
}
}
}
} }
} }
@ -112,4 +173,12 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
Principal().apply { displayName = "user2" }, Principal().apply { displayName = "user2" },
)) ))
} }
companion object {
val CaldavAccount.canRemovePrincipal: Boolean
get() = when (serverType) {
SERVER_TASKS, SERVER_OWNCLOUD, SERVER_SABREDAV -> true
else -> false
}
}
} }

@ -0,0 +1,37 @@
package org.tasks.caldav
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.Principal
import org.tasks.data.PrincipalDao
import javax.inject.Inject
@HiltViewModel
class CaldavCalendarViewModel @Inject constructor(
private val provider: CaldavClientProvider,
private val principalDao: PrincipalDao
) : ViewModel() {
val error = MutableLiveData<Throwable?>()
val inFlight = MutableLiveData(false)
suspend fun remove(account: CaldavAccount, list: CaldavCalendar, principal: Principal) =
withContext(NonCancellable) {
if (inFlight.value == true) {
return@withContext
}
inFlight.value = true
try {
provider.forAccount(account).removePrincipal(account, list, principal)
principalDao.delete(principal)
} catch (e: Exception) {
error.value = e
} finally {
inFlight.value = false
}
}
}

@ -1,7 +1,9 @@
package org.tasks.caldav package org.tasks.caldav
import at.bitfire.cert4android.CustomCertManager import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.DavResource.Companion.MIME_XML
import at.bitfire.dav4jvm.Property import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation import at.bitfire.dav4jvm.Response.HrefRelation
@ -10,21 +12,38 @@ import at.bitfire.dav4jvm.XmlUtils.NS_CALDAV
import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV
import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.* import at.bitfire.dav4jvm.property.CalendarColor
import at.bitfire.dav4jvm.property.CalendarHomeSet
import at.bitfire.dav4jvm.property.CurrentUserPrincipal
import at.bitfire.dav4jvm.property.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.dav4jvm.property.ResourceType.Companion.CALENDAR import at.bitfire.dav4jvm.property.ResourceType.Companion.CALENDAR
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.SyncToken
import com.todoroo.astrid.helper.UUIDHelper 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.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.property.Invite import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCInvite import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.PropertyUtils.NS_OWNCLOUD
import org.tasks.caldav.property.ShareAccess import org.tasks.caldav.property.ShareAccess
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
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.CaldavCalendar
import org.tasks.data.Principal
import org.tasks.ui.DisplayableException import org.tasks.ui.DisplayableException
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
@ -216,7 +235,52 @@ open class CaldavClient(
return this return this
} }
suspend fun removePrincipal(
account: CaldavAccount,
calendar: CaldavCalendar,
principal: Principal,
) {
when (account.serverType) {
SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, principal)
SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, principal)
else -> throw IllegalArgumentException()
}
}
private suspend fun removeOwncloudPrincipal(calendar: CaldavCalendar, principal: Principal) =
withContext(Dispatchers.IO) {
DavCollection(httpClient, calendar.url!!.toHttpUrl())
.post(
"""
<x4:share xmlns:x4="$NS_OWNCLOUD">
<x4:remove>
<x0:href xmlns:x0="$NS_WEBDAV">${principal.principal}</x0:href>
</x4:remove>
</x4:share>
""".trimIndent().toRequestBody(MIME_XML)
) {}
}
private suspend fun removeSabrePrincipal(calendar: CaldavCalendar, principal: Principal) =
withContext(Dispatchers.IO) {
DavCollection(httpClient, calendar.url!!.toHttpUrl())
.post(
"""
<D:share-resource xmlns:D="$NS_WEBDAV">
<D:sharee>
<D:href>${principal.principal}</D:href>
<D:share-access>
<D:no-access />
</D:share-access>
</D:sharee>
</D:share-resource>
""".trimIndent().toRequestBody(MEDIATYPE_SHARING)
) {}
}
companion object { companion object {
private val MEDIATYPE_SHARING = "application/davsharing+xml".toMediaType()
private val calendarProperties = arrayOf( private val calendarProperties = arrayOf(
ResourceType.NAME, ResourceType.NAME,
DisplayName.NAME, DisplayName.NAME,

@ -43,7 +43,7 @@ class CaldavCalendar : Parcelable {
var order = NO_ORDER var order = NO_ORDER
@ColumnInfo(name = "cdl_access") @ColumnInfo(name = "cdl_access")
var access = 0 var access = ACCESS_OWNER
constructor() constructor()

@ -15,6 +15,9 @@ WHERE principal_list = :list
AND principal NOT IN (:principals)""") AND principal NOT IN (:principals)""")
fun deleteRemoved(list: Long, principals: List<String>) fun deleteRemoved(list: Long, principals: List<String>)
@Delete
fun delete(principal: Principal)
@Delete @Delete
fun delete(principals: List<Principal>) fun delete(principals: List<Principal>)

@ -689,4 +689,6 @@ File %1$s contained %2$s.\n\n
<string name="account_not_included">Not included with \'Name your price\' subscriptions</string> <string name="account_not_included">Not included with \'Name your price\' subscriptions</string>
<string name="map_theme_use_app_theme">Use app theme</string> <string name="map_theme_use_app_theme">Use app theme</string>
<string name="list_members">List members</string> <string name="list_members">List members</string>
<string name="remove_user">Remove user?</string>
<string name="remove_user_confirmation">%1$s will no longer have access to %2$s</string>
</resources> </resources>

Loading…
Cancel
Save