Compare commits

...

8 Commits

Author SHA1 Message Date
Ricki Hirner
17602f89d6 Version bump to 2.4-beta1 2019-03-23 12:17:43 +01:00
Ricki Hirner
039593b9e6 About: use vector icon 2019-03-20 22:20:12 +01:00
Ricki Hirner
baaeb343dd Create calendar: new UI, use ViewModel 2019-03-19 21:28:18 +01:00
Ricki Hirner
2164088e1d Delete collection: use ViewModel 2019-03-19 21:28:18 +01:00
Ricki Hirner
94f8cb72c9 AboutActivity: use ViewModel 2019-03-17 17:07:37 +01:00
Ricki Hirner
8e51c3ac9a Update beta feedback email address, libraries 2019-03-17 16:32:20 +01:00
Ricki Hirner
d9af394610 Login: couple user name and email address 2019-03-17 16:10:47 +01:00
Ricki Hirner
7ff0e55546 Account setup: fix crash 2019-03-16 15:02:57 +01:00
22 changed files with 551 additions and 337 deletions

View File

@@ -19,7 +19,7 @@ android {
defaultConfig {
applicationId "at.bitfire.davdroid"
versionCode 272
versionCode 274
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
buildConfigField "boolean", "customCerts", "true"
@@ -42,7 +42,7 @@ android {
flavorDimensions "distribution"
productFlavors {
standard {
versionName "2.3-ose"
versionName "2.4-beta1-ose"
}
}
@@ -89,6 +89,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') {

View File

@@ -14,7 +14,9 @@ import android.os.Parcelable
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.ical4android.MiscUtils
import okhttp3.HttpUrl
/**
@@ -44,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
@@ -152,6 +155,8 @@ data class CollectionInfo(
return values
}
fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)
private fun getAsBooleanOrNull(values: ContentValues, field: String): Boolean? {
val i = values.getAsInteger(field)
@@ -191,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)
@@ -237,6 +243,7 @@ data class CollectionInfo(
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),

View File

@@ -8,28 +8,31 @@
package at.bitfire.davdroid.ui
import android.content.Context
import android.app.Application
import android.os.Build
import android.os.Bundle
import android.text.Spanned
import android.util.DisplayMetrics
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.loader.app.LoaderManager
import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.about_davdroid.*
import kotlinx.android.synthetic.main.about.*
import kotlinx.android.synthetic.main.activity_about.*
import org.apache.commons.io.IOUtils
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
class AboutActivity: AppCompatActivity() {
@@ -44,7 +47,6 @@ class AboutActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
setSupportActionBar(toolbar)
@@ -72,66 +74,63 @@ class AboutActivity: AppCompatActivity() {
override fun getPageTitle(position: Int): String =
when (position) {
1 -> getString(R.string.about_libraries)
else -> getString(R.string.app_name)
0 -> getString(R.string.app_name)
else -> getString(R.string.about_libraries)
}
override fun getItem(position: Int) =
when (position) {
1 -> LibsBuilder()
0 -> AppFragment()
else -> LibsBuilder()
.withAutoDetect(false)
.withFields(R.string::class.java.fields)
.withLicenseShown(true)
.supportFragment()
else -> DavdroidFragment()
}!!
}
class DavdroidFragment: Fragment(), LoaderManager.LoaderCallbacks<Spanned> {
class AppFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.about_davdroid, container, false)!!
inflater.inflate(R.layout.about, container, false)!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
app_name.text = getString(R.string.app_name)
app_version.text = getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
build_time.text = getString(R.string.about_build_date, SimpleDateFormat.getDateInstance().format(BuildConfig.buildTime))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
icon.setImageDrawable(resources.getDrawableForDensity(R.mipmap.ic_launcher, DisplayMetrics.DENSITY_XXXHIGH))
pixels.text = HtmlCompat.fromHtml(pixelsHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
if (true /* open-source version */) {
warranty.setText(R.string.about_license_info_no_warranty)
LoaderManager.getInstance(this).initLoader(0, null, this)
val model = ViewModelProviders.of(this).get(LicenseModel::class.java)
model.htmlText.observe(this, Observer { spanned ->
license_text.text = spanned
})
}
}
override fun onCreateLoader(id: Int, args: Bundle?) =
HtmlAssetLoader(requireActivity(), "gplv3.html")
override fun onLoadFinished(loader: Loader<Spanned>, license: Spanned?) {
Logger.log.info("LOAD FINISHED")
license_text.text = license
}
override fun onLoaderReset(loader: Loader<Spanned>) {
}
}
class HtmlAssetLoader(
context: Context,
val fileName: String
): AsyncTaskLoader<Spanned>(context) {
class LicenseModel(
application: Application
): AndroidViewModel(application) {
override fun onStartLoading() {
forceLoad()
}
val htmlText = MutableLiveData<Spanned>()
override fun loadInBackground(): Spanned =
context.resources.assets.open(fileName).use {
HtmlCompat.fromHtml(IOUtils.toString(it, Charsets.UTF_8), HtmlCompat.FROM_HTML_MODE_LEGACY)
init {
thread {
getApplication<Application>().resources.assets.open("gplv3.html").use {
val spanned = HtmlCompat.fromHtml(IOUtils.toString(it, Charsets.UTF_8), HtmlCompat.FROM_HTML_MODE_LEGACY)
htmlText.postValue(spanned)
}
}
}
}

View File

@@ -216,7 +216,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
SetReadOnlyTask(WeakReference(this), info.id!!, nowChecked).execute()
}
R.id.delete_collection ->
DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
DeleteCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
R.id.properties ->
CollectionInfoFragment.newInstance(info).show(supportFragmentManager, null)
}

View File

@@ -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)
account = intent.extras.getParcelable(EXTRA_ACCOUNT)!!
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setContentView(R.layout.activity_create_calendar)
color.setOnClickListener { _ ->
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()
})
val binding = DataBindingUtil.setContentView<ActivityCreateCalendarBinding>(this, R.layout.activity_create_calendar)
binding.lifecycleOwner = this
binding.model = model
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 cal = Calendar()
cal.components += tz.vTimeZone
info.timeZone = cal.toString()
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()
}
model.timezoneError.value = null
}
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
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
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 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) {
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()
}
}
if (ok) {
info.type = CollectionInfo.Type.CALENDAR
CreateCollectionFragment.newInstance(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?) {
val timeZones = TimeZone.getAvailableIDs()
time_zone.adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, timeZones)
class TimeZoneInfo(
val id: String,
val displayName: String
) {
override fun toString() = id
}
// 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
var account: Account? = null
val displayName = MutableLiveData<String>()
val displayNameError = MutableLiveData<String>()
val description = MutableLiveData<String>()
val color = MutableLiveData<Int>()
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
}
info?.let {
home_sets.adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, info.homeSets)
}
}
color.value = Constants.DAVDROID_GREEN_RGBA
override fun onLoaderReset(loader: Loader<AccountInfo>) {}
timezone.value = TimeZone.getDefault().id
supportVEVENT.value = true
supportVTODO.value = true
supportVJOURNAL.value = true
class AccountInfo {
val homeSets = LinkedList<String>()
}
class AccountInfoLoader(
context: Context,
val account: Account
): AsyncTaskLoader<AccountInfo>(context) {
override fun onStartLoading() = forceLoad()
override fun loadInBackground(): AccountInfo? {
val info = AccountInfo()
ServiceDB.OpenHelper(context).use { dbHelper ->
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
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)
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()) {
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())
adapter.add(c.getString(0))
}
}
}
homeSets.postValue(adapter)
idxHomeSet.postValue(0)
}
}
return info
}
}
}

View File

@@ -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")
}

View File

@@ -21,7 +21,7 @@ import at.bitfire.davdroid.R
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
companion object {
private const val BETA_FEEDBACK_URI = "mailto:support@davx5.com?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
private const val BETA_FEEDBACK_URI = "mailto:play@bitfire.at?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
}

View File

@@ -9,137 +9,120 @@
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 androidx.appcompat.app.AlertDialog
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.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.DeleteCollectionBinding
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.settings.AccountSettings
import kotlin.concurrent.thread
class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<Exception> {
class DeleteCollectionFragment: DialogFragment() {
companion object {
const val ARG_ACCOUNT = "account"
const val ARG_COLLECTION_INFO = "collectionInfo"
fun newInstance(account: Account, collectionInfo: CollectionInfo): DialogFragment {
val frag = DeleteCollectionFragment()
val args = Bundle(2)
args.putParcelable(ARG_ACCOUNT, account)
args.putParcelable(ARG_COLLECTION_INFO, collectionInfo)
frag.arguments = args
return frag
}
}
private lateinit var account: Account
private lateinit var collectionInfo: CollectionInfo
private lateinit var model: DeleteCollectionModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProviders.of(this).get(DeleteCollectionModel::class.java)
account = arguments!!.getParcelable(ARG_ACCOUNT)!!
collectionInfo = arguments!!.getParcelable(ARG_COLLECTION_INFO)!!
LoaderManager.getInstance(this).initLoader(0, null, this)
model.account = arguments?.getParcelable(ARG_ACCOUNT) as? Account
model.collectionInfo = arguments?.getParcelable(ARG_COLLECTION_INFO) as? CollectionInfo
}
@Suppress("DEPRECATION")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val progress = ProgressDialog(context)
progress.setTitle(R.string.delete_collection_deleting_collection)
progress.setMessage(getString(R.string.please_wait))
progress.isIndeterminate = true
progress.setCanceledOnTouchOutside(false)
isCancelable = false
return progress
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = DeleteCollectionBinding.inflate(layoutInflater, null, false)
binding.lifecycleOwner = this
binding.model = model
binding.ok.setOnClickListener {
isCancelable = false
binding.progress.visibility = View.VISIBLE
binding.controls.visibility = View.GONE
model.deleteCollection().observe(this, Observer { exception ->
if (exception == null)
// reload collection list
(activity as? AccountActivity)?.reload()
else
requireFragmentManager().beginTransaction()
.add(ExceptionInfoFragment.newInstance(exception, model.account), null)
.commit()
dismiss()
})
}
binding.cancel.setOnClickListener {
dismiss()
}
return binding.root
}
override fun onCreateLoader(id: Int, args: Bundle?) =
DeleteCollectionLoader(activity!!, account, collectionInfo)
class DeleteCollectionModel(
application: Application
): AndroidViewModel(application) {
override fun onLoadFinished(loader: Loader<Exception>, exception: Exception?) {
dismiss()
var account: Account? = null
var collectionInfo: CollectionInfo? = null
if (exception != null)
requireFragmentManager().beginTransaction()
.add(ExceptionInfoFragment.newInstance(exception, account), null)
.commit()
else
(activity as? AccountActivity)?.reload()
}
val confirmation = MutableLiveData<Boolean>()
val result = MutableLiveData<Exception>()
override fun onLoaderReset(loader: Loader<Exception>) {}
fun deleteCollection(): LiveData<Exception> {
thread {
val account = requireNotNull(account)
val collectionInfo = requireNotNull(collectionInfo)
val context = getApplication<Application>()
HttpClient.Builder(context, AccountSettings(context, account))
.setForeground(true)
.build().use { httpClient ->
try {
val collection = DavResource(httpClient.okHttpClient, collectionInfo.url)
class DeleteCollectionLoader(
context: Context,
val account: Account,
val collectionInfo: CollectionInfo
): AsyncTaskLoader<Exception>(context) {
// delete collection from server
collection.delete(null) {}
override fun onStartLoading() = forceLoad()
// delete collection locally
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.writableDatabase
db.delete(ServiceDB.Collections._TABLE, "${ServiceDB.Collections.ID}=?", arrayOf(collectionInfo.id.toString()))
}
override fun loadInBackground(): Exception? {
HttpClient.Builder(context, AccountSettings(context, account))
.setForeground(true)
.build().use { httpClient ->
try {
val collection = DavResource(httpClient.okHttpClient, collectionInfo.url)
// return success
result.postValue(null)
// delete collection from server
collection.delete(null) {}
// delete collection locally
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.writableDatabase
db.delete(ServiceDB.Collections._TABLE, "${ServiceDB.Collections.ID}=?", arrayOf(collectionInfo.id.toString()))
}
} catch(e: Exception) {
return e
}
} catch(e: Exception) {
// return error
result.postValue(e)
}
}
}
return null
}
}
class ConfirmDeleteCollectionFragment: DialogFragment() {
companion object {
fun newInstance(account: Account, collectionInfo: CollectionInfo): DialogFragment {
val frag = ConfirmDeleteCollectionFragment()
val args = Bundle(2)
args.putParcelable(ARG_ACCOUNT, account)
args.putParcelable(ARG_COLLECTION_INFO, collectionInfo)
frag.arguments = args
return frag
}
return result
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val collectionInfo = arguments!!.getParcelable(ARG_COLLECTION_INFO) as CollectionInfo
val name = if (collectionInfo.displayName.isNullOrBlank())
collectionInfo.url.toString()
else
collectionInfo.displayName
return AlertDialog.Builder(activity!!)
.setTitle(R.string.delete_collection_confirm_title)
.setMessage(getString(R.string.delete_collection_confirm_warning, name))
.setPositiveButton(android.R.string.yes) { _, _ ->
val frag = DeleteCollectionFragment()
frag.arguments = arguments
frag.show(fragmentManager, null)
}
.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
}
.create()
}
}
}

View File

@@ -90,19 +90,19 @@ class DefaultLoginCredentialsFragment: Fragment() {
when {
model.loginWithEmailAddress.value == true -> {
// login with email address
model.emailAddressError.value = null
val email = model.emailAddress.value.orEmpty()
model.usernameError.value = null
val email = model.username.value.orEmpty()
if (email.matches(Regex(".+@.+"))) {
// already looks like an email address
try {
loginModel.baseURI = URI(MailTo.MAILTO_SCHEME, email, null)
valid = true
} catch (e: URISyntaxException) {
model.emailAddressError.value = e.localizedMessage
model.usernameError.value = e.localizedMessage
}
} else {
valid = false
model.emailAddressError.value = getString(R.string.login_email_address_error)
model.usernameError.value = getString(R.string.login_email_address_error)
}
val password = validatePassword()

View File

@@ -15,9 +15,7 @@ class DefaultLoginCredentialsModel: ViewModel() {
val baseUrl = MutableLiveData<String>()
val baseUrlError = MutableLiveData<String>()
val emailAddress = MutableLiveData<String>()
val emailAddressError = MutableLiveData<String>()
/** user name or email address */
val username = MutableLiveData<String>()
val usernameError = MutableLiveData<String>()
@@ -45,7 +43,7 @@ class DefaultLoginCredentialsModel: ViewModel() {
baseUrl.value = givenUrl
} else {
loginWithEmailAddress.value = true
emailAddress.value = givenUsername
username.value = givenUsername
}
password.value = givenPassword

View File

@@ -59,7 +59,7 @@ class DetectConfigurationFragment: Fragment() {
inflater.inflate(R.layout.detect_configuration, container, false)!!
private class DetectConfigurationModel(
class DetectConfigurationModel(
application: Application
): AndroidViewModel(application) {

View File

@@ -11,7 +11,6 @@ object BindingAdapters {
@JvmStatic
fun setError(textView: TextView, error: String?) {
textView.error = error
textView.requestFocus()
}
@BindingAdapter("html")
@@ -24,4 +23,4 @@ object BindingAdapters {
textView.text = null
}
}
}

View File

@@ -11,6 +11,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_horizontal">
<LinearLayout
@@ -20,10 +21,12 @@
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="128dp"
android:layout_height="128dp"
android:src="@mipmap/ic_launcher"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_launcher_foreground"
android:layout_marginBottom="16dp"/>
<TextView

View File

@@ -7,118 +7,143 @@
~ 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">
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin">
<data>
<import type="android.view.View"/>
<variable
name="model"
type="at.bitfire.davdroid.ui.CreateCalendarActivity.Model"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/create_calendar"
android:textAppearance="@style/TextView.Heading"/>
<ScrollView
android:layout_width="match_parent"
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_collection_home_set"/>
<Spinner
android:id="@+id/home_sets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@id/display_name"
android:text="@string/create_collection_display_name"/>
<EditText
android:id="@+id/display_name"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/create_calendar_display_name_hint"/>
android:layout_height="wrap_content">
<TextView
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:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:inputType="textAutoCorrect"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/create_collection_display_name"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/color">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={model.displayName}"
app:error="@{model.displayNameError}" />
</com.google.android.material.textfield.TextInputLayout>
<View
android:id="@+id/color"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="8dp"
android:background="@color/secondaryColor"/>
android:layout_marginLeft="8dp"
android:background="@{model.color, default=@color/primaryColor}"
android:contentDescription="@string/create_collection_color"
app:layout_constraintStart_toEndOf="@id/display_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/display_name"
app:layout_constraintBottom_toBottomOf="@id/display_name"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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: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:text="@string/create_collection_color"/>
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:adapter="@{model.homeSets}"
android:selectedItemPosition="@={model.idxHomeSet}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/homesets_title" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/create_calendar_time_zone"/>
<Spinner
android:id="@+id/time_zone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/create_calendar_type"
android:textAppearance="@style/TextView.Heading"/>
<RadioGroup
android:id="@+id/type"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/type_events"
<TextView
android:id="@+id/timezone_title"
android:labelFor="@id/timezone"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/create_calendar_time_zone"
app:layout_constraintTop_toBottomOf="@id/homeset"
app:layout_constraintStart_toStartOf="parent" />
<AutoCompleteTextView
android:id="@+id/timezone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/create_calendar_type_only_events"/>
android:text="@={model.timezone}"
app:error="@{model.timezoneError}"
app:layout_constraintTop_toBottomOf="@+id/timezone_title"
app:layout_constraintStart_toStartOf="parent"/>
<RadioButton
android:id="@+id/type_tasks"
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/create_calendar_type"
app:error="@{model.typeError}"
app:layout_constraintTop_toBottomOf="@id/timezone"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/create_calendar_type_only_tasks"/>
app:flexWrap="wrap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/type">
<RadioButton
android:id="@+id/type_events_and_tasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/create_calendar_type_events_and_tasks"/>
<CheckBox
android:id="@+id/support_vevent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:checked="@={model.supportVEVENT}"
android:text="@string/create_calendar_type_vevent"/>
<CheckBox
android:id="@+id/support_vtodo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:checked="@={model.supportVTODO}"
android:text="@string/create_calendar_type_vtodo"/>
<CheckBox
android:id="@+id/support_vjournal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:checked="@={model.supportVJOURNAL}"
android:text="@string/create_calendar_type_vjournal"/>
</RadioGroup>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</ScrollView>
</layout>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="at.bitfire.davdroid.ui.DeleteCollectionFragment.DeleteCollectionModel" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<TextView
style="@style/TextView.Heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete_collection_confirm_title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{@string/delete_collection_confirm_warning(model.collectionInfo.title())}" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_marginBottom="8dp"
android:visibility="gone"
tools:visibility="visible"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>
<LinearLayout
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={model.confirmation}"
android:layout_marginBottom="8dp"
android:text="@string/delete_collection_data_shall_be_deleted"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:orientation="horizontal">
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
android:text="@android:string/cancel"/>
<Button
android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
android:enabled="@{model.confirmation ?? false}"
android:text="@android:string/ok"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</layout>

View File

@@ -65,8 +65,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:text="@={model.emailAddress}"
app:error="@{model.emailAddressError}"
android:text="@={model.username}"
app:error="@{model.usernameError}"
android:inputType="textEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout

View File

@@ -10,7 +10,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/create_collection_create"
<item android:id="@+id/create_calendar"
android:title="@string/create_collection_create"
android:onClick="onCreateCollection"
app:showAsAction="always"/>

View File

@@ -248,23 +248,26 @@
<!-- collection management -->
<string name="create_addressbook">Create address book</string>
<string name="create_addressbook_display_name_hint">My Address Book</string>
<string name="create_calendar">Create CalDAV collection</string>
<string name="create_calendar_display_name_hint">My Calendar</string>
<string name="create_calendar_time_zone">Time zone:</string>
<string name="create_calendar_type">Collection type:</string>
<string name="create_calendar_type_only_events">Calendar (only events)</string>
<string name="create_calendar_type_only_tasks">Task list (only tasks)</string>
<string name="create_calendar">Create calendar</string>
<string name="create_calendar_time_zone">Time zone</string>
<string name="create_calendar_time_zone_required">Time zone required</string>
<string name="create_calendar_type">Possible calendar entries</string>
<string name="create_calendar_type_vevent">Events</string>
<string name="create_calendar_type_vtodo">Tasks</string>
<string name="create_calendar_type_vjournal">Notes / journal</string>
<string name="create_calendar_type_events_and_tasks">Combined (events and tasks)</string>
<string name="create_collection_color">Set a collection color</string>
<string name="create_collection_color">Color</string>
<string name="create_collection_creating">Creating collection</string>
<string name="create_collection_display_name">Display name (title) of this collection:</string>
<string name="create_collection_display_name">Title</string>
<string name="create_collection_display_name_required">Title is required</string>
<string name="create_collection_description">Description (optional):</string>
<string name="create_collection_home_set">Home set:</string>
<string name="create_collection_description">Description</string>
<string name="create_collection_optional">optional</string>
<string name="create_collection_home_set">Storage location</string>
<string name="create_collection_create">Create</string>
<string name="delete_collection">Delete collection</string>
<string name="delete_collection_confirm_title">Are you sure?</string>
<string name="delete_collection_confirm_warning">This collection (%s) and all its data will be removed from the server.</string>
<string name="delete_collection_data_shall_be_deleted">These data shall be deleted permanently.</string>
<string name="delete_collection_deleting_collection">Deleting collection</string>
<string name="collection_force_read_only">Force read-only</string>
<string name="collection_properties">Properties</string>