Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9df0db1e authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Correctly handle permissions in WebcalFragment

parent f6f3ebb4
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -267,7 +267,6 @@ class AccountActivity: AppCompatActivity() {
            thread {
                cardDavService.postValue(db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CARDDAV))
                calDavService.postValue(db.serviceDao().getIdByAccountAndType(account.name, Service.TYPE_CALDAV))
                //servicesLoaded.postValue(null)
            }
        }

+130 −45
Original line number Diff line number Diff line
@@ -3,6 +3,8 @@ package at.bitfire.davdroid.ui.account
import android.Manifest
import android.app.Activity
import android.app.Application
import android.content.ContentProviderClient
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.ContentObserver
@@ -18,8 +20,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.*
import androidx.room.Transaction
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.Constants
@@ -31,6 +32,7 @@ import at.bitfire.davdroid.model.Collection
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.account_caldav_item.view.*
import okhttp3.HttpUrl
import java.util.logging.Level

class WebcalFragment: CollectionsFragment() {

@@ -40,9 +42,20 @@ class WebcalFragment: CollectionsFragment() {
        super.onCreate(savedInstanceState)

        webcalModel = ViewModelProviders.of(this).get(WebcalModel::class.java)
        webcalModel.calendarPermission.observe(this, Observer { granted ->
            if (!granted)
                requestPermissions(arrayOf(Manifest.permission.READ_CALENDAR), 0)
        })
        webcalModel.subscribedUrls.observe(this, Observer { urls ->
            Logger.log.log(Level.FINE, "Got calendar list", urls.keys)
        })

        webcalModel.initialize(arguments?.getLong(EXTRA_SERVICE_ID) ?: throw IllegalArgumentException("EXTRA_SERVICE_ID required"))
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) =
            webcalModel.calendarPermission.check()

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
            inflater.inflate(R.menu.caldav_actions, menu)

@@ -136,72 +149,134 @@ class WebcalFragment: CollectionsFragment() {
        private var initialized = false
        private var serviceId: Long = 0

        private val workerThread = HandlerThread(javaClass.simpleName)
        init { workerThread.start() }
        val workerHandler = Handler(workerThread.looper)

        private val db = AppDatabase.getInstance(application)
        private val resolver = application.contentResolver

        private val calendarPermissions = ContextCompat.checkSelfPermission(application, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED
        private val calendarProvider = if (calendarPermissions) resolver.acquireContentProviderClient(CalendarContract.AUTHORITY) else null
        val calendarPermission = CalendarPermission(application)
        private val calendarProvider = object: MediatorLiveData<ContentProviderClient>() {
            var havePermission = false

        private val subscribedUrls = mutableMapOf<HttpUrl, Long>()
            init {
                addSource(calendarPermission) { granted ->
                    havePermission = granted
                    if (granted)
                        connect()
                    else
                        disconnect()
                }
            }

        private val workerThread = HandlerThread(javaClass.simpleName)
        init { workerThread.start() }
        val workerHandler = Handler(workerThread.looper)
            override fun onActive() {
                super.onActive()
                connect()
            }

        val observer = object: ContentObserver(workerHandler) {
            override fun onChange(selfChange: Boolean) {
                queryCalendars()
            fun connect() {
                if (havePermission && value == null)
                    value = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
            }

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

        fun initialize(dbServiceId: Long) {
            if (initialized)
                return
            initialized = true
            fun disconnect() {
                value?.closeCompat()
                value = null
            }
        }
        val subscribedUrls = object: MediatorLiveData<MutableMap<HttpUrl, Long>>() {
            var provider: ContentProviderClient? = null
            var observer: ContentObserver? = null

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

            serviceId = dbServiceId
            override fun onActive() {
                super.onActive()
                connect()
            }

            if (calendarPermissions)
                resolver.registerContentObserver(Calendars.CONTENT_URI, false, observer)
            private fun connect() {
                unregisterObserver()
                provider?.let { provider ->
                    val newObserver = object: ContentObserver(workerHandler) {
                        override fun onChange(selfChange: Boolean) {
                            queryCalendars(provider)
                        }
                    }
                    getApplication<Application>().contentResolver.registerContentObserver(Calendars.CONTENT_URI, false, newObserver)
                    observer = newObserver

                    workerHandler.post {
                queryCalendars()
                        queryCalendars(provider)
                    }
                }
            }

        override fun onCleared() {
            if (calendarPermissions)
                resolver.unregisterContentObserver(observer)
            override fun onInactive() {
                super.onInactive()
                unregisterObserver()
            }

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

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

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

                postValue(subscriptions)
            }
        }


        fun initialize(dbServiceId: Long) {
            if (initialized)
                return
            initialized = true

            serviceId = dbServiceId
            calendarPermission.check()
        }

        fun unsubscribe(webcal: Collection) {
            workerHandler.post {
                subscribedUrls[webcal.source]?.let { id ->
                subscribedUrls.value?.get(webcal.source)?.let { id ->
                    // delete subscription from Android calendar list
                    calendarProvider?.delete(Calendars.CONTENT_URI,
                    calendarProvider.value?.delete(Calendars.CONTENT_URI,
                            "${Calendars._ID}=?", arrayOf(id.toString()))
                }
            }
@@ -209,4 +284,14 @@ class WebcalFragment: CollectionsFragment() {

    }

    class CalendarPermission(val context: Context): LiveData<Boolean>() {
        init {
            check()
        }

        fun check() {
            value = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED
        }
    }

}
 No newline at end of file