Loading app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.kt +74 −63 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) } } } } } } app/src/main/java/at/bitfire/davdroid/ui/CreateCalendarActivity.kt +10 −41 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() + "/")!!) Loading Loading @@ -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) { Loading Loading @@ -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 Loading @@ -277,11 +244,13 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener { } } } if (!adapter.isEmpty) { homeSets.postValue(adapter) idxHomeSet.postValue(0) } } } } } Loading app/src/main/java/at/bitfire/davdroid/ui/CreateCollectionFragment.kt +143 −149 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -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() Loading Loading @@ -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 } } } app/src/main/java/at/bitfire/davdroid/ui/HomesetAdapter.kt 0 → 100644 +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 app/src/main/res/layout/activity_create_address_book.xml +68 −49 Original line number Diff line number Diff line Loading @@ -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
app/src/main/java/at/bitfire/davdroid/ui/CreateAddressBookActivity.kt +74 −63 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) } } } } } }
app/src/main/java/at/bitfire/davdroid/ui/CreateCalendarActivity.kt +10 −41 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() + "/")!!) Loading Loading @@ -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) { Loading Loading @@ -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 Loading @@ -277,11 +244,13 @@ class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener { } } } if (!adapter.isEmpty) { homeSets.postValue(adapter) idxHomeSet.postValue(0) } } } } } Loading
app/src/main/java/at/bitfire/davdroid/ui/CreateCollectionFragment.kt +143 −149 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -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() Loading Loading @@ -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 } } }
app/src/main/java/at/bitfire/davdroid/ui/HomesetAdapter.kt 0 → 100644 +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
app/src/main/res/layout/activity_create_address_book.xml +68 −49 Original line number Diff line number Diff line Loading @@ -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