Skip to content

Commit

Permalink
(Un)subscribe from AccountActivity's WebcalFragment
Browse files Browse the repository at this point in the history
  • Loading branch information
rfc2822 committed Oct 17, 2023
1 parent 6bc4349 commit 562f019
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 149 deletions.
1 change: 1 addition & 0 deletions app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ abstract class AppDatabase: RoomDatabase() {
abstract fun collectionDao(): CollectionDao
abstract fun principalDao(): PrincipalDao
abstract fun syncStatsDao(): SyncStatsDao
abstract fun webcalSubscriptionDao(): WebcalSubscriptionDao
abstract fun webDavDocumentDao(): WebDavDocumentDao
abstract fun webDavMountDao(): WebDavMountDao

Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ interface CollectionDao {
@Update
fun update(collection: Collection)

@Query("UPDATE collection SET sync=:sync WHERE id=:collectionId")
fun updateSync(collectionId: Long, sync: Boolean)

/**
* Tries to insert new row, but updates existing row if already present.
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
Expand Down
29 changes: 27 additions & 2 deletions app/src/main/kotlin/at/bitfire/davdroid/db/WebcalSubscription.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.util.DavUtils
import okhttp3.HttpUrl

/**
Expand Down Expand Up @@ -32,10 +34,33 @@ data class WebcalSubscription(

var url: HttpUrl,
var displayName: String? = null,
var color: Long,
var color: Int,

var eTag: String? = null,
var lastModified: Long? = null,
var lastSynchronized: Long? = null,
var error: String? = null
)
) {

companion object {

/**
* Converts a CalDAV collection info that represents a Webcal subscription
* to a [WebcalSubscription] object.
*
* @param collection CalDAV collection (of type [Collection.TYPE_WEBCAL]])
* @return subscription data object
*/
fun fromCollection(collection: Collection) =
WebcalSubscription(
id = 0,
collectionId = collection.id,
calendarId = null,
url = collection.url,
displayName = collection.displayName,
color = collection.color ?: Constants.DAVDROID_GREEN_RGBA
)

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package at.bitfire.davdroid.db

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction

@Dao
interface WebcalSubscriptionDao {

@Query("SELECT * FROM webcal_subscription WHERE collectionId=:collectionId")
fun getByCollectionId(collectionId: Long): WebcalSubscription?

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(subscription: WebcalSubscription): Long

@Delete
fun delete(subscription: WebcalSubscription)


@Transaction
fun insertAndUpdateCollection(collectionDao: CollectionDao, subscription: WebcalSubscription) {
insert(subscription)
subscription.collectionId?.let { collectionId ->
collectionDao.updateSync(collectionId, true)
}
}

}
169 changes: 22 additions & 147 deletions app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,27 @@

package at.bitfire.davdroid.ui.account

import android.Manifest
import android.content.ContentProviderClient
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.view.*
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
import androidx.lifecycle.*
import androidx.room.Transaction
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AccountCaldavItemBinding
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.db.WebcalSubscription
import at.bitfire.davdroid.util.PermissionUtils
import com.google.android.material.snackbar.Snackbar
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.util.logging.Level
import javax.inject.Inject
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set

@AndroidEntryPoint
class WebcalFragment: CollectionsFragment() {
Expand Down Expand Up @@ -71,14 +53,6 @@ class WebcalFragment: CollectionsFragment() {
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

webcalModel.subscribedUrls.observe(this, Observer { urls ->
Logger.log.log(Level.FINE, "Got Android calendar list", urls.keys)
})
}

override fun onResume() {
super.onResume()
requireActivity().addMenuProvider(menuProvider)
Expand Down Expand Up @@ -136,6 +110,17 @@ class WebcalFragment: CollectionsFragment() {
}

private fun subscribe(item: Collection) {
AlertDialog.Builder(webcalFragment.requireActivity())
.setItems(R.array.webcal_subscribe_directly_or_external) { _, btn ->
when (btn) {
0 -> webcalModel.subscribe(item)
1 -> subscribeExternal(item)
}
}
.show()
}

private fun subscribeExternal(item: Collection) {
var uri = Uri.parse(item.source.toString())
when {
uri.scheme.equals("http", true) -> uri = uri.buildUpon().scheme("webcal").build()
Expand All @@ -146,8 +131,6 @@ class WebcalFragment: CollectionsFragment() {
item.displayName?.let { intent.putExtra("title", it) }
item.color?.let { intent.putExtra("color", it) }

Logger.log.info("Intent: ${intent.extras}")

val activity = webcalFragment.requireActivity()
if (activity.packageManager.resolveActivity(intent, 0) != null)
activity.startActivity(intent)
Expand All @@ -162,7 +145,6 @@ class WebcalFragment: CollectionsFragment() {

snack.show()
}

}

}
Expand All @@ -174,13 +156,12 @@ class WebcalFragment: CollectionsFragment() {
): CollectionAdapter(accountModel) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
CalendarViewHolder(parent, accountModel, webcalModel, webcalFragment)
CalendarViewHolder(parent, accountModel, webcalModel, webcalFragment)

}


class WebcalModel @AssistedInject constructor(
@ApplicationContext context: Context,
val db: AppDatabase,
@Assisted val serviceId: Long
): ViewModel() {
Expand All @@ -190,123 +171,17 @@ class WebcalFragment: CollectionsFragment() {
fun create(serviceId: Long): WebcalModel
}

private val resolver = context.contentResolver

private var calendarPermission = false
private val calendarProvider = object: MediatorLiveData<ContentProviderClient>() {
init {
calendarPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED
if (calendarPermission)
connect()
}

override fun onActive() {
super.onActive()
connect()
}

fun connect() {
if (calendarPermission && value == null)
value = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
}

override fun onInactive() {
super.onInactive()
disconnect()
}

fun disconnect() {
value?.close()
value = null
}
}
val subscribedUrls = object: MediatorLiveData<MutableMap<Long, HttpUrl>>() {
var provider: ContentProviderClient? = null
var observer: ContentObserver? = null

init {
addSource(calendarProvider) { provider ->
this.provider = provider
if (provider != null) {
connect()
} else
unregisterObserver()
}
}

override fun onActive() {
super.onActive()
connect()
}

private fun connect() {
unregisterObserver()
provider?.let { provider ->
val newObserver = object: ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
queryCalendars(provider)
}
}
}
context.contentResolver.registerContentObserver(Calendars.CONTENT_URI, false, newObserver)
observer = newObserver

viewModelScope.launch(Dispatchers.IO) {
queryCalendars(provider)
}
}
}

override fun onInactive() {
super.onInactive()
unregisterObserver()
}
private val dao = db.webcalSubscriptionDao()

private fun unregisterObserver() {
observer?.let {
context.contentResolver.unregisterContentObserver(it)
observer = null
}
}

@WorkerThread
@Transaction
private fun queryCalendars(provider: ContentProviderClient) {
// query subscribed URLs from Android calendar list
val subscriptions = mutableMapOf<Long, HttpUrl>()
provider.query(Calendars.CONTENT_URI, arrayOf(Calendars._ID, Calendars.NAME),null, null, null)?.use { cursor ->
while (cursor.moveToNext())
cursor.getString(1)?.let { rawName ->
rawName.toHttpUrlOrNull()?.let { url ->
subscriptions[cursor.getLong(0)] = url
}
}
}

// update "sync" field in database accordingly (will update UI)
db.collectionDao().getByServiceAndType(serviceId, Collection.TYPE_WEBCAL).forEach { webcal ->
val newSync = subscriptions.values
.any { webcal.source?.let { source -> UrlUtils.equals(source, it) } ?: false }
if (newSync != webcal.sync)
db.collectionDao().update(webcal.copy(sync = newSync))
}

postValue(subscriptions)
}
fun subscribe(item: Collection) = viewModelScope.launch(Dispatchers.IO) {
val subscription = WebcalSubscription.fromCollection(item)
dao.insertAndUpdateCollection(db.collectionDao(), subscription)
}


fun unsubscribe(webcal: Collection) {
viewModelScope.launch(Dispatchers.IO) {
// find first matching source (Webcal) URL
subscribedUrls.value?.entries?.firstOrNull { (_, source) ->
UrlUtils.equals(source, webcal.source!!)
}?.key?.let { id ->
// delete first matching subscription from Android calendar list
calendarProvider.value?.delete(Calendars.CONTENT_URI,
"${Calendars._ID}=?", arrayOf(id.toString()))
}
fun unsubscribe(item: Collection) = viewModelScope.launch(Dispatchers.IO) {
dao.getByCollectionId(item.id)?.let { subscription ->
dao.delete(subscription)
db.collectionDao().updateSync(item.id, false)
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,10 @@

<!-- Webcal subscriptions -->
<string name="webcal_subscriptions">Webcal subscriptions</string>
<string-array name="webcal_subscribe_directly_or_external">
<item>Subscribe directly</item>
<item>Subscribe with external app like ICSx⁵</item>
</string-array>

<!-- WebDAV accounts -->
<string name="webdav_authority" translatable="false">at.bitfire.davdroid.webdav</string>
Expand Down

0 comments on commit 562f019

Please sign in to comment.