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

Commit bff9c87c authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Create collections: use ViewModel

parent d27261f1
Loading
Loading
Loading
Loading
+74 −63
Original line number Diff line number Diff line
@@ -9,43 +9,48 @@
package at.bitfire.davdroid.ui

import android.accounts.Account
import android.content.Context
import android.app.Application
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
import android.widget.SpinnerAdapter
import androidx.annotation.MainThread
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.ViewModelProviders
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityCreateAddressBookBinding
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import kotlinx.android.synthetic.main.activity_create_address_book.*
import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
import java.util.*
import kotlin.concurrent.thread

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

    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)

        account = intent.getParcelableExtra(EXTRA_ACCOUNT)

        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        setContentView(R.layout.activity_create_address_book)

        LoaderManager.getInstance(this).initLoader(0, intent.extras, this)
        model = ViewModelProviders.of(this).get(Model::class.java)
        (intent?.getParcelableExtra(EXTRA_ACCOUNT) as? Account)?.let {
            model.initialize(it)
        }

        val binding = DataBindingUtil.setContentView<ActivityCreateAddressBookBinding>(this, R.layout.activity_create_address_book)
        binding.lifecycleOwner = this
        binding.model = model
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -56,77 +61,83 @@ class CreateAddressBookActivity: AppCompatActivity(), LoaderManager.LoaderCallba
    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? ?: return
        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.description = StringUtils.trimToNull(model.description.value)

            if (ok) {
                info.type = CollectionInfo.Type.ADDRESS_BOOK
                CreateCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
                CreateCollectionFragment.newInstance(model.account!!, info).show(supportFragmentManager, null)
            }
        }
    }


    override fun onCreateLoader(id: Int, args: Bundle?) = AccountInfoLoader(this, account)
    class Model(
            application: Application
    ) : AndroidViewModel(application) {

    override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo?) {
        info?.let {
            home_sets.adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, it.homeSets)
        }
    }
        var account: Account? = null

    override fun onLoaderReset(loader: Loader<AccountInfo>) {
    }
        val displayName = MutableLiveData<String>()
        val displayNameError = MutableLiveData<String>()

    class AccountInfo {
        val homeSets = LinkedList<String>()
    }
        val description = MutableLiveData<String>()

    class AccountInfoLoader(
            context: Context,
            val account: Account
    ): AsyncTaskLoader<AccountInfo>(context) {
        val homeSets = MutableLiveData<SpinnerAdapter>()
        val idxHomeSet = MutableLiveData<Int>()

        override fun onStartLoading() = forceLoad()
        @MainThread
        fun initialize(account: Account) {
            if (this.account != null)
                return
            this.account = account

        override fun loadInBackground(): AccountInfo? {
            val info = AccountInfo()
            ServiceDB.OpenHelper(context).use { dbHelper ->
                // find DAV service and home sets
            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_CARDDAV), 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))
                            }
                        }
                    }
            return info
                    if (!adapter.isEmpty) {
                        homeSets.postValue(adapter)
                        idxHomeSet.postValue(0)
                    }
                }
            }
        }

    }

}
+10 −41
Original line number Diff line number Diff line
@@ -14,15 +14,12 @@ 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.annotation.MainThread
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NavUtils
import androidx.databinding.DataBindingUtil
@@ -106,7 +103,7 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
    fun onCreateCollection(item: MenuItem) {
        var ok = true

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

@@ -154,35 +151,6 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
        }
    }

    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 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
        }

    }

    class TimeZoneAdapter(
            context: Context
    ): ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, android.R.id.text1) {
@@ -245,12 +213,11 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
        val supportVTODO = MutableLiveData<Boolean>()
        val supportVJOURNAL = MutableLiveData<Boolean>()

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

            color.value = Constants.DAVDROID_GREEN_RGBA

@@ -277,11 +244,13 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
                            }
                        }
                    }
                    if (!adapter.isEmpty) {
                        homeSets.postValue(adapter)
                        idxHomeSet.postValue(0)
                    }
                }
            }
        }

    }

+143 −149
Original line number Diff line number Diff line
@@ -9,14 +9,13 @@
package at.bitfire.davdroid.ui

import android.accounts.Account
import android.app.Dialog
import android.app.ProgressDialog
import android.content.Context
import android.app.Application
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader
import androidx.lifecycle.*
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.davdroid.DavUtils
@@ -29,8 +28,9 @@ import at.bitfire.davdroid.settings.AccountSettings
import java.io.IOException
import java.io.StringWriter
import java.util.logging.Level
import kotlin.concurrent.thread

class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<Exception> {
class CreateCollectionFragment: DialogFragment() {

    companion object {

@@ -48,59 +48,88 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<

    }

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

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

        val args = requireNotNull(arguments)
        account = args.getParcelable(ARG_ACCOUNT)!!
        info = args.getParcelable(ARG_COLLECTION_INFO)!!
        model = ViewModelProviders.of(this).get(Model::class.java)
        model.account = arguments?.getParcelable(ARG_ACCOUNT) ?: throw IllegalArgumentException()
        model.info = arguments?.getParcelable(ARG_COLLECTION_INFO) ?: throw IllegalArgumentException()

        LoaderManager.getInstance(this).initLoader(0, null, this)
        model.createCollection().observe(this, Observer { exception ->
            if (exception != null)
                requireFragmentManager().beginTransaction()
                        .add(ExceptionInfoFragment.newInstance(exception, model.account), null)
                        .commit()
            else
                requireActivity().finish()
        })
    }

    @Suppress("DEPRECATION")
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val progress = ProgressDialog(context)
        progress.setTitle(R.string.create_collection_creating)
        progress.setMessage(getString(R.string.please_wait))
        progress.isIndeterminate = true
        progress.setCanceledOnTouchOutside(false)
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val v = inflater.inflate(R.layout.create_collection, container, false)
        isCancelable = false
        return progress
        return v
    }


    override fun onCreateLoader(id: Int, args: Bundle?) = CreateCollectionLoader(requireActivity(), account, info)
    class Model(
            application: Application
    ): AndroidViewModel(application) {

    override fun onLoadFinished(loader: Loader<Exception>, exception: Exception?) {
        dismiss()
        lateinit var account: Account
        lateinit var info: CollectionInfo

        activity?.let { parent ->
            if (exception != null)
                requireFragmentManager().beginTransaction()
                        .add(ExceptionInfoFragment.newInstance(exception, account), null)
                        .commit()
            else
                parent.finish()
        }
        val result = MutableLiveData<Exception>()

    }
        fun createCollection(): LiveData<Exception> {
            thread {
                HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account))
                        .setForeground(true)
                        .build().use { httpClient ->
                    try {
                        val collection = DavResource(httpClient.okHttpClient, info.url)

    override fun onLoaderReset(loader: Loader<Exception>) {}
                        // create collection on remote server
                        collection.mkCol(generateXml()) {}

                        // no HTTP error -> create collection locally
                        ServiceDB.OpenHelper(getApplication()).use { dbHelper ->
                            val db = dbHelper.writableDatabase

    class CreateCollectionLoader(
            context: Context,
            val account: Account,
            val info: CollectionInfo
    ): AsyncTaskLoader<Exception>(context) {
                            // 1. find service ID
                            val serviceType = when (info.type) {
                                CollectionInfo.Type.ADDRESS_BOOK -> ServiceDB.Services.SERVICE_CARDDAV
                                CollectionInfo.Type.CALENDAR -> ServiceDB.Services.SERVICE_CALDAV
                                else -> throw IllegalArgumentException("Collection must be an address book or calendar")
                            }
                            db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
                                    "${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
                                    arrayOf(account.name, serviceType), null, null, null).use { c ->

                                assert(c.moveToNext())
                                val serviceID = c.getLong(0)

                                // 2. add collection to service
                                val values = info.toDB()
                                values.put(ServiceDB.Collections.SERVICE_ID, serviceID)
                                db.insert(ServiceDB.Collections._TABLE, null, values)
                            }
                        }

        override fun onStartLoading() = forceLoad()
                        // post success
                        result.postValue(null)
                    } catch (e: Exception) {
                        // post error
                        result.postValue(e)
                    }
                }
            }
            return result
        }

        override fun loadInBackground(): Exception? {
        fun generateXml(): String {
            val writer = StringWriter()
            try {
                val serializer = XmlUtils.newSerializer()
@@ -188,44 +217,9 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
                Logger.log.log(Level.SEVERE, "Couldn't assemble Extended MKCOL request", e)
            }

            HttpClient.Builder(context, AccountSettings(context, account))
                    .setForeground(true)
                    .build().use { httpClient ->
                try {
                    val collection = DavResource(httpClient.okHttpClient, info.url)

                    // create collection on remote server
                    collection.mkCol(writer.toString()) {}

                    // now insert collection into database:
                    ServiceDB.OpenHelper(context).use { dbHelper ->
                        val db = dbHelper.writableDatabase

                        // 1. find service ID
                        val serviceType = when (info.type) {
                            CollectionInfo.Type.ADDRESS_BOOK -> ServiceDB.Services.SERVICE_CARDDAV
                            CollectionInfo.Type.CALENDAR     -> ServiceDB.Services.SERVICE_CALDAV
                            else -> throw IllegalArgumentException("Collection must be an address book or calendar")
            return writer.toString()
        }
                        db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
                                "${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
                                arrayOf(account.name, serviceType), null, null, null).use { c ->

                            assert(c.moveToNext())
                            val serviceID = c.getLong(0)

                            // 2. add collection to service
                            val values = info.toDB()
                            values.put(ServiceDB.Collections.SERVICE_ID, serviceID)
                            db.insert(ServiceDB.Collections._TABLE, null, values)
                        }
                    }
                } catch(e: Exception) {
                    return e
                }
            }
            return null
        }
    }

}
+37 −0
Original line number Diff line number Diff line
package at.bitfire.davdroid.ui

import android.content.Context
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView

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 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
    }

}
 No newline at end of file
+68 −49
Original line number Diff line number Diff line
@@ -7,55 +7,74 @@
  ~ http://www.gnu.org/licenses/gpl.html
  -->

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_height="match_parent"
            android:layout_width="match_parent">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="android.view.View"/>
        <variable
            name="model"
            type="at.bitfire.davdroid.ui.CreateAddressBookActivity.Model"/>
    </data>

    <LinearLayout android:orientation="vertical"
    <ScrollView
        android:layout_width="match_parent"
                  android:layout_height="wrap_content"
        android:layout_height="match_parent"
        android:padding="@dimen/activity_margin">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/create_addressbook"
            android:textAppearance="@style/TextView.Heading"/>
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <TextView
            android:layout_width="wrap_content"
            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/display_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
            android:text="@string/create_collection_home_set"/>
        <Spinner
            android:id="@+id/home_sets"
            android:layout_width="wrap_content"
                android:hint="@string/create_collection_display_name"
                app:layout_constraintHorizontal_weight="1"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent">
                <com.google.android.material.textfield.TextInputEditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"/>
                    android:text="@={model.displayName}"
                    app:error="@{model.displayNameError}" />
            </com.google.android.material.textfield.TextInputLayout>

        <TextView
            android:layout_width="wrap_content"
            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/description"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
            android:labelFor="@+id/display_name"
            android:text="@string/create_collection_display_name"/>
        <EditText
            android:id="@id/display_name"
                android:hint="@string/create_collection_description"
                app:helperText="@string/create_collection_optional"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/display_name">
                <com.google.android.material.textfield.TextInputEditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:hint="@string/create_addressbook_display_name_hint"/>
                    android:text="@={model.description}" />
            </com.google.android.material.textfield.TextInputLayout>

            <TextView
                android:id="@+id/homesets_title"
                android:labelFor="@id/homeset"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
            android:labelFor="@+id/description"
            android:text="@string/create_collection_description"/>
        <EditText
            android:id="@id/description"
            android:layout_width="match_parent"
                android:text="@string/create_collection_home_set"
                android:layout_marginTop="16dp"
                app:layout_constraintTop_toBottomOf="@+id/description"
                app:layout_constraintStart_toStartOf="parent" />
            <Spinner
                android:id="@+id/homeset"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:inputType="textAutoCorrect"/>
                android:adapter="@{model.homeSets}"
                android:selectedItemPosition="@={model.idxHomeSet}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/homesets_title" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </LinearLayout>
    </ScrollView>

</layout>
 No newline at end of file
Loading