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

Commit 8fe093d9 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Create calendar: new UI, use ViewModel

parent e99acb29
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -91,6 +91,8 @@ dependencies {
    implementation 'androidx.lifecycle:lifecycle-livedata:2.0.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
    implementation 'androidx.preference:preference:1.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android:flexbox:1.1.0'
    implementation 'com.google.android.material:material:1.0.0'

    implementation(':dav4jvm') {
+3 −0
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ data class CollectionInfo(
        var timeZone: String? = null,
        var supportsVEVENT: Boolean = false,
        var supportsVTODO: Boolean = false,
        var supportsVJOURNAL: Boolean = false,
        var selected: Boolean = false,

        // subscriptions
@@ -195,6 +196,7 @@ data class CollectionInfo(
        dest.writeString(timeZone)
        dest.writeByte(if (supportsVEVENT) 1 else 0)
        dest.writeByte(if (supportsVTODO) 1 else 0)
        dest.writeByte(if (supportsVJOURNAL) 1 else 0)
        dest.writeByte(if (selected) 1 else 0)

        dest.writeString(source)
@@ -241,6 +243,7 @@ data class CollectionInfo(
                    parcel.readByte() != 0.toByte(),
                    parcel.readByte() != 0.toByte(),
                    parcel.readByte() != 0.toByte(),
                    parcel.readByte() != 0.toByte(),

                    parcel.readString(),

+187 −78
Original line number Diff line number Diff line
@@ -9,19 +9,30 @@
package at.bitfire.davdroid.ui

import android.accounts.Account
import android.app.Application
import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.SpinnerAdapter
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NavUtils
import androidx.loader.app.LoaderManager
import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityCreateCalendarBinding
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.ical4android.DateUtils
@@ -32,35 +43,46 @@ import net.fortuna.ical4j.model.Calendar
import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
import java.util.*
import kotlin.concurrent.thread

class CreateCalendarActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<CreateCalendarActivity.AccountInfo>, ColorPickerDialogListener {
class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {

    companion object {
        const val EXTRA_ACCOUNT = "account"
    }

    private lateinit var account: Account
    private lateinit var model: Model

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

        account = intent.extras.getParcelable(EXTRA_ACCOUNT)!!
        model = ViewModelProviders.of(this).get(Model::class.java)
        (intent?.extras?.getParcelable(EXTRA_ACCOUNT) as? Account)?.let {
            model.initialize(it)
        }
        model.homeSets.observe(this, Observer {
            if (it.isEmpty)
                // no known homesets, we don't know where to create the calendar
                finish()
        })

        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        val binding = DataBindingUtil.setContentView<ActivityCreateCalendarBinding>(this, R.layout.activity_create_calendar)
        binding.lifecycleOwner = this
        binding.model = model

        setContentView(R.layout.activity_create_calendar)
        color.setOnClickListener { _ ->
        binding.color.setOnClickListener { _ ->
            ColorPickerDialog.newBuilder()
                    .setShowAlphaSlider(false)
                    .setColor((color.background as ColorDrawable).color)
                    .show(this)
        }

        LoaderManager.getInstance(this).initLoader(0, null, this)
        binding.timezone.setAdapter(model.timezones)
    }

    override fun onColorSelected(dialogId: Int, rgb: Int) {
        color.setBackgroundColor(rgb)
        model.color.value = rgb
    }

    override fun onDialogDismissed(dialogId: Int) {
@@ -75,105 +97,192 @@ class CreateCalendarActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks
    override fun onOptionsItemSelected(item: MenuItem) =
            if (item.itemId == android.R.id.home) {
                val intent = Intent(this, AccountActivity::class.java)
                intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
                intent.putExtra(AccountActivity.EXTRA_ACCOUNT, model.account)
                NavUtils.navigateUpTo(this, intent)
                true
            } else
                false

    fun onCreateCollection(item: MenuItem) {
        val homeSet = home_sets.selectedItem as String

        var ok = true
        HttpUrl.parse(homeSet)?.let {
            val info = CollectionInfo(it.resolve(UUID.randomUUID().toString() + "/")!!)
            info.displayName = display_name.text.toString()
            if (info.displayName.isNullOrBlank()) {
                display_name.error = getString(R.string.create_collection_display_name_required)

        val parent = model.homeSets.value?.getItem(model.idxHomeSet.value!!) as String
        HttpUrl.parse(parent)?.let { parentUrl ->
            val info = CollectionInfo(parentUrl.resolve(UUID.randomUUID().toString() + "/")!!)

            val displayName = model.displayName.value
            if (displayName.isNullOrBlank()) {
                model.displayNameError.value = getString(R.string.create_collection_display_name_required)
                ok = false
            } else {
                info.displayName = displayName
                model.displayNameError.value = null
            }

            info.description = StringUtils.trimToNull(description.text.toString())
            info.color = (color.background as ColorDrawable).color
            info.description = StringUtils.trimToNull(model.description.value)
            info.color = model.color.value

            DateUtils.tzRegistry.getTimeZone(time_zone.selectedItem as String)?.let { tz ->
            val tzId = model.timezone.value
            if (tzId.isNullOrBlank()) {
                model.timezoneError.value = getString(R.string.create_calendar_time_zone_required)
                ok = false
            } else {
                DateUtils.tzRegistry.getTimeZone(tzId)?.let { tz ->
                    val cal = Calendar()
                    cal.components += tz.vTimeZone
                    info.timeZone = cal.toString()
                }

            when (type.checkedRadioButtonId) {
                R.id.type_events ->
                    info.supportsVEVENT = true
                R.id.type_tasks ->
                    info.supportsVTODO = true
                R.id.type_events_and_tasks -> {
                    info.supportsVEVENT = true
                    info.supportsVTODO = true
                }
                model.timezoneError.value = null
            }

            if (ok) {
            val supportsVEVENT = model.supportVEVENT.value ?: false
            val supportsVTODO = model.supportVTODO.value ?: false
            val supportsVJOURNAL = model.supportVJOURNAL.value ?: false
            if (!supportsVEVENT && !supportsVTODO && !supportsVJOURNAL) {
                ok = false
                model.typeError.value = ""
            } else
                model.typeError.value = null

            info.type = CollectionInfo.Type.CALENDAR
                CreateCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
            info.supportsVEVENT = supportsVEVENT
            info.supportsVTODO = supportsVTODO
            info.supportsVJOURNAL = supportsVJOURNAL

            if (ok)
                CreateCollectionFragment.newInstance(model.account!!, info).show(supportFragmentManager, null)
        }
    }

    class HomesetAdapter(
            context: Context
    ): ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, android.R.id.text1) {

        init {
            setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        }

        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
            val data = getItem(position)!!
            val v = super.getView(position, convertView, parent)
            v.findViewById<TextView>(android.R.id.text1).apply {
                setSingleLine()
                ellipsize = TextUtils.TruncateAt.START
            }
            return v
        }

    override fun onCreateLoader(id: Int, args: Bundle?) = AccountInfoLoader(this, account)
        override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
            val data = getItem(position)!!
            val v = super.getDropDownView(position, convertView, parent)
            v.findViewById<TextView>(android.R.id.text1).apply {
                ellipsize = TextUtils.TruncateAt.START
            }
            return v
        }

    override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo?) {
        val timeZones = TimeZone.getAvailableIDs()
        time_zone.adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, timeZones)
    }

        // select system time zone
        val defaultTimeZone = TimeZone.getDefault().id
        for (i in 0 until timeZones.size)
            if (timeZones[i] == defaultTimeZone) {
                time_zone.setSelection(i)
                break
    class TimeZoneAdapter(
            context: Context
    ): ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, android.R.id.text1) {

        val tz = TimeZone.getAvailableIDs()

        override fun getFilter(): Filter {
            return object: Filter() {
                override fun performFiltering(constraint: CharSequence?): FilterResults {
                    val filtered = constraint?.let {
                        tz.filter { it.contains(constraint, true) }
                    } ?: listOf()
                    val results = FilterResults()
                    results.values = filtered
                    results.count = filtered.size
                    return results
                }
                override fun publishResults(constraint: CharSequence?, results: FilterResults) {
                    clear()
                    @Suppress("UNCHECKED_CAST") addAll(results.values as List<String>)
                    if (results.count >= 0)
                        notifyDataSetChanged()
                    else
                        notifyDataSetInvalidated()
                }
            }
        }

        info?.let {
            home_sets.adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, info.homeSets)
    }


    class Model(
            application: Application
    ): AndroidViewModel(application) {

        class TimeZoneInfo(
                val id: String,
                val displayName: String
        ) {
            override fun toString() = id
        }

    override fun onLoaderReset(loader: Loader<AccountInfo>) {}
        var account: Account? = null

        val displayName = MutableLiveData<String>()
        val displayNameError = MutableLiveData<String>()

        val description = MutableLiveData<String>()
        val color = MutableLiveData<Int>()

    class AccountInfo {
        val homeSets = LinkedList<String>()
        val homeSets = MutableLiveData<SpinnerAdapter>()
        val idxHomeSet = MutableLiveData<Int>()

        val timezones = TimeZoneAdapter(application)
        val timezone = MutableLiveData<String>()
        val timezoneError = MutableLiveData<String>()

        val typeError = MutableLiveData<String>()
        val supportVEVENT = MutableLiveData<Boolean>()
        val supportVTODO = MutableLiveData<Boolean>()
        val supportVJOURNAL = MutableLiveData<Boolean>()

        fun initialize(account: Account) {
            synchronized(this) {
                if (this.account != null)
                    return
                this.account = account
            }

    class AccountInfoLoader(
            context: Context,
            val account: Account
    ): AsyncTaskLoader<AccountInfo>(context) {
            color.value = Constants.DAVDROID_GREEN_RGBA

            timezone.value = TimeZone.getDefault().id

        override fun onStartLoading() = forceLoad()
            supportVEVENT.value = true
            supportVTODO.value = true
            supportVJOURNAL.value = true

        override fun loadInBackground(): AccountInfo? {
            val info = AccountInfo()
            ServiceDB.OpenHelper(context).use { dbHelper ->
            thread {
                // load account info
                ServiceDB.OpenHelper(getApplication()).use { dbHelper ->
                    val adapter = HomesetAdapter(getApplication())
                    val db = dbHelper.readableDatabase
                    db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
                            "${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
                            arrayOf(account.name, ServiceDB.Services.SERVICE_CALDAV), null, null, null).use { cursor ->
                    if (!cursor.moveToNext())
                        return null
                        if (cursor.moveToNext()) {
                            val strServiceID = cursor.getString(0)

                            db.query(ServiceDB.HomeSets._TABLE, arrayOf(ServiceDB.HomeSets.URL),
                                    "${ServiceDB.HomeSets.SERVICE_ID}=?", arrayOf(strServiceID), null, null, null).use { c ->
                                while (c.moveToNext())
                            info.homeSets += c.getString(0)
                                    adapter.add(c.getString(0))
                            }
                        }
                    }
                    homeSets.postValue(adapter)
                    idxHomeSet.postValue(0)
                }
            return info
            }
        }

    }

}
+5 −0
Original line number Diff line number Diff line
@@ -171,6 +171,11 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
                                        attribute(null, "name", "VTODO")
                                        endTag(XmlUtils.NS_CALDAV, "comp")
                                    }
                                    if (info.supportsVJOURNAL) {
                                        startTag(XmlUtils.NS_CALDAV, "comp")
                                        attribute(null, "name", "VJOURNAL")
                                        endTag(XmlUtils.NS_CALDAV, "comp")
                                    }
                                    endTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set")
                                }
    
+122 −97

File changed.

Preview size limit exceeded, changes collapsed.

Loading