mirror of https://github.com/etesync/android
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
821 lines
33 KiB
821 lines
33 KiB
package com.etesync.syncadapter.ui
|
|
|
|
import android.accounts.Account
|
|
import android.accounts.AccountManager
|
|
import android.app.Dialog
|
|
import android.app.ProgressDialog
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.provider.ContactsContract
|
|
import android.text.TextUtils
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.widget.*
|
|
import androidx.activity.viewModels
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.fragment.app.*
|
|
import at.bitfire.ical4android.Event
|
|
import at.bitfire.ical4android.Task
|
|
import at.bitfire.vcard4android.Contact
|
|
import at.bitfire.vcard4android.ContactsStorageException
|
|
import com.etebase.client.Account as EtebaseAccount
|
|
import com.etebase.client.Client
|
|
import com.etebase.client.Item
|
|
import com.etebase.client.ItemMetadata
|
|
import com.etesync.journalmanager.model.SyncEntry
|
|
import com.etesync.syncadapter.*
|
|
import com.etesync.syncadapter.model.*
|
|
import com.etesync.syncadapter.resource.LocalAddressBook
|
|
import com.etesync.syncadapter.resource.LocalCalendar
|
|
import com.etesync.syncadapter.ui.etebase.*
|
|
import com.etesync.syncadapter.ui.setup.CreateAccountFragment
|
|
import com.etesync.syncadapter.ui.setup.LoginCredentials
|
|
import com.google.android.material.textfield.TextInputEditText
|
|
import com.google.android.material.textfield.TextInputLayout
|
|
import net.cachapa.expandablelayout.ExpandableLayout
|
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
import okhttp3.Request
|
|
import okhttp3.RequestBody
|
|
import org.jetbrains.anko.doAsync
|
|
import org.jetbrains.anko.uiThread
|
|
import java.io.StringReader
|
|
import java.net.URI
|
|
import java.util.*
|
|
import kotlin.collections.HashMap
|
|
|
|
class MigrateV2Activity : BaseActivity() {
|
|
private lateinit var accountV1: Account
|
|
private val model: AccountViewModel by viewModels()
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
accountV1 = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
|
|
|
|
setContentView(R.layout.etebase_fragment_activity)
|
|
|
|
if (savedInstanceState == null) {
|
|
setTitle(R.string.migrate_v2_wizard_welcome_title)
|
|
supportFragmentManager.commit {
|
|
replace(R.id.fragment_container, WizardWelcomeFragment.newInstance(accountV1))
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private val EXTRA_ACCOUNT = "account"
|
|
|
|
fun newIntent(context: Context, account: Account): Intent {
|
|
val intent = Intent(context, MigrateV2Activity::class.java)
|
|
intent.putExtra(EXTRA_ACCOUNT, account)
|
|
return intent
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fun reportErrorHelper(context: Context, e: Throwable) {
|
|
AlertDialog.Builder(context)
|
|
.setIcon(R.drawable.ic_info_dark)
|
|
.setTitle(R.string.exception)
|
|
.setMessage(e.localizedMessage)
|
|
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
|
|
}
|
|
|
|
class WizardWelcomeFragment : Fragment() {
|
|
internal lateinit var accountV1: Account
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
val ret = inflater.inflate(R.layout.migrate_v2_wizard_welcome, container, false)
|
|
|
|
if (savedInstanceState == null) {
|
|
if (container != null) {
|
|
initUi(inflater, ret)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
private fun initUi(inflater: LayoutInflater, v: View) {
|
|
v.findViewById<Button>(R.id.signup).setOnClickListener {
|
|
parentFragmentManager.commit {
|
|
replace(R.id.fragment_container, SignupFragment.newInstance(accountV1))
|
|
addToBackStack(null)
|
|
}
|
|
}
|
|
|
|
v.findViewById<Button>(R.id.login).setOnClickListener {
|
|
parentFragmentManager.commit {
|
|
replace(R.id.fragment_container, LoginFragment.newInstance(accountV1))
|
|
addToBackStack(null)
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account): WizardWelcomeFragment {
|
|
val ret = WizardWelcomeFragment()
|
|
ret.accountV1 = accountV1
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
class SignupFragment : Fragment() {
|
|
internal lateinit var editUserName: TextInputLayout
|
|
internal lateinit var editEmail: TextInputLayout
|
|
internal lateinit var editPassword: TextInputLayout
|
|
|
|
internal lateinit var showAdvanced: CheckedTextView
|
|
internal lateinit var customServer: TextInputEditText
|
|
|
|
internal lateinit var accountV1: Account
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
val v = inflater.inflate(R.layout.signup_fragment, container, false)
|
|
|
|
editUserName = v.findViewById(R.id.user_name)
|
|
editEmail = v.findViewById(R.id.email)
|
|
editPassword = v.findViewById(R.id.url_password)
|
|
showAdvanced = v.findViewById(R.id.show_advanced)
|
|
customServer = v.findViewById(R.id.custom_server)
|
|
|
|
// Hide stuff we don't need for the migration tool
|
|
v.findViewById<View>(R.id.trial_notice).visibility = View.GONE
|
|
editEmail.visibility = View.GONE
|
|
editEmail.editText?.setText(accountV1.name)
|
|
|
|
val login = v.findViewById<Button>(R.id.login)
|
|
login.visibility = View.GONE
|
|
|
|
val createAccount = v.findViewById<Button>(R.id.create_account)
|
|
createAccount.setOnClickListener {
|
|
val credentials = validateData()
|
|
if (credentials != null) {
|
|
SignupDoFragment.newInstance(accountV1, credentials).show(requireFragmentManager(), null)
|
|
}
|
|
}
|
|
|
|
val advancedLayout = v.findViewById<View>(R.id.advanced_layout) as ExpandableLayout
|
|
|
|
showAdvanced.setOnClickListener {
|
|
if (showAdvanced.isChecked) {
|
|
showAdvanced.isChecked = false
|
|
advancedLayout.collapse()
|
|
} else {
|
|
showAdvanced.isChecked = true
|
|
advancedLayout.expand()
|
|
}
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
protected fun validateData(): SignupCredentials? {
|
|
var valid = true
|
|
|
|
val userName = editUserName.editText?.text.toString()
|
|
// FIXME: this validation should only be done in the server, we are doing it here until the Java library supports field errors
|
|
if ((userName.length < 6) || (!userName.matches(Regex("""^[\w.-]+$""")))) {
|
|
editUserName.error = getString(R.string.login_username_error)
|
|
valid = false
|
|
} else {
|
|
editUserName.error = null
|
|
}
|
|
|
|
val email = editEmail.editText?.text.toString()
|
|
if (email.isEmpty()) {
|
|
editEmail.error = getString(R.string.login_email_address_error)
|
|
valid = false
|
|
} else {
|
|
editEmail.error = null
|
|
}
|
|
|
|
val password = editPassword.editText?.text.toString()
|
|
if (password.length < 8) {
|
|
editPassword.error = getString(R.string.signup_password_restrictions)
|
|
valid = false
|
|
} else {
|
|
editPassword.error = null
|
|
}
|
|
|
|
var uri: URI? = null
|
|
if (showAdvanced.isChecked) {
|
|
val server = customServer.text.toString()
|
|
// If this field is null, just use the default
|
|
if (!server.isEmpty()) {
|
|
val url = server.toHttpUrlOrNull()
|
|
if (url != null) {
|
|
uri = url.toUri()
|
|
customServer.error = null
|
|
} else {
|
|
customServer.error = getString(R.string.login_custom_server_error)
|
|
valid = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return if (valid) SignupCredentials(uri, userName, email, password) else null
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account): SignupFragment {
|
|
val ret = SignupFragment()
|
|
ret.accountV1 = accountV1
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
class SignupDoFragment : DialogFragment() {
|
|
private val model: ConfigurationViewModel by activityViewModels()
|
|
|
|
internal lateinit var accountV1: Account
|
|
internal lateinit var signupCredentials: SignupCredentials
|
|
|
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
val progress = ProgressDialog(activity)
|
|
progress.setTitle(R.string.setting_up_encryption)
|
|
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
|
progress.isIndeterminate = true
|
|
progress.setCanceledOnTouchOutside(false)
|
|
isCancelable = false
|
|
return progress
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
if (savedInstanceState == null) {
|
|
val settings = AccountSettings(requireContext(), accountV1)
|
|
// Mark the etesync v1 account as wanting migration
|
|
doAsync {
|
|
val httpClient = HttpClient.Builder(context, settings).setForeground(true).build().okHttpClient
|
|
val remote = settings.uri!!.toHttpUrlOrNull()!!.newBuilder()
|
|
.addPathSegments("etesync-v2/confirm-migration/")
|
|
.build()
|
|
|
|
val body = RequestBody.create(null, byteArrayOf())
|
|
|
|
val request = Request.Builder()
|
|
.post(body)
|
|
.url(remote)
|
|
.build()
|
|
|
|
val response = httpClient.newCall(request).execute()
|
|
uiThread {
|
|
if (context == null) {
|
|
dismissAllowingStateLoss()
|
|
return@uiThread
|
|
}
|
|
if (response.isSuccessful) {
|
|
model.signup(requireContext(), signupCredentials)
|
|
} else {
|
|
if (response.code == 400) {
|
|
reportErrorHelper(requireContext(), Error("User already migrated. Please login instead."))
|
|
} else {
|
|
reportErrorHelper(requireContext(), Error("Failed preparing account for migration"))
|
|
}
|
|
dismissAllowingStateLoss()
|
|
}
|
|
}
|
|
}
|
|
model.observe(this) {
|
|
if (it.isFailed) {
|
|
reportErrorHelper(requireContext(), it.error!!)
|
|
dismissAllowingStateLoss()
|
|
} else {
|
|
doAsync {
|
|
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
|
|
val client = Client.create(httpClient, it.url.toString())
|
|
val etebase = EtebaseAccount.restore(client, it.etebaseSession!!, null)
|
|
uiThread {
|
|
fragmentManager?.commit {
|
|
replace(R.id.fragment_container, WizardCollectionsFragment.newInstance(accountV1, etebase))
|
|
addToBackStack(null)
|
|
}
|
|
dismissAllowingStateLoss()
|
|
}
|
|
}
|
|
} }
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account, signupCredentials: SignupCredentials): SignupDoFragment {
|
|
val ret = SignupDoFragment()
|
|
ret.accountV1 = accountV1
|
|
ret.signupCredentials = signupCredentials
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class LoginFragment() : Fragment() {
|
|
internal lateinit var editUserName: EditText
|
|
internal lateinit var editUrlPassword: TextInputLayout
|
|
|
|
internal lateinit var showAdvanced: CheckedTextView
|
|
internal lateinit var customServer: EditText
|
|
|
|
internal lateinit var accountV1: Account
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
|
|
|
|
editUserName = v.findViewById<TextInputEditText>(R.id.user_name)
|
|
editUrlPassword = v.findViewById<TextInputLayout>(R.id.url_password)
|
|
showAdvanced = v.findViewById<CheckedTextView>(R.id.show_advanced)
|
|
customServer = v.findViewById<TextInputEditText>(R.id.custom_server)
|
|
|
|
v.findViewById<View>(R.id.create_account).visibility = View.GONE
|
|
|
|
val login = v.findViewById<View>(R.id.login) as Button
|
|
login.setOnClickListener {
|
|
val credentials = validateLoginData()
|
|
if (credentials != null)
|
|
LoginDoFragment.newInstance(accountV1, credentials).show(fragmentManager!!, null)
|
|
}
|
|
|
|
val forgotPassword = v.findViewById<View>(R.id.forgot_password) as TextView
|
|
forgotPassword.setOnClickListener { WebViewActivity.openUrl(context!!, Constants.forgotPassword) }
|
|
|
|
val advancedLayout = v.findViewById<View>(R.id.advanced_layout) as ExpandableLayout
|
|
|
|
showAdvanced.setOnClickListener {
|
|
if (showAdvanced.isChecked) {
|
|
showAdvanced.isChecked = false
|
|
advancedLayout.collapse()
|
|
} else {
|
|
showAdvanced.isChecked = true
|
|
advancedLayout.expand()
|
|
}
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
protected fun validateLoginData(): LoginCredentials? {
|
|
var valid = true
|
|
|
|
val userName = editUserName.text.toString()
|
|
if (userName.isEmpty()) {
|
|
editUserName.error = getString(R.string.login_email_address_error)
|
|
valid = false
|
|
} else {
|
|
editUserName.error = null
|
|
}
|
|
|
|
val password = editUrlPassword.editText?.text.toString()
|
|
if (password.isEmpty()) {
|
|
editUrlPassword.error = getString(R.string.login_password_required)
|
|
valid = false
|
|
} else {
|
|
editUrlPassword.error = null
|
|
}
|
|
|
|
var uri: URI? = null
|
|
if (showAdvanced.isChecked) {
|
|
val server = customServer.text.toString()
|
|
// If this field is null, just use the default
|
|
if (!server.isEmpty()) {
|
|
val url = server.toHttpUrlOrNull()
|
|
if (url != null) {
|
|
uri = url.toUri()
|
|
customServer.error = null
|
|
} else {
|
|
customServer.error = getString(R.string.login_custom_server_error)
|
|
valid = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return if (valid) LoginCredentials(uri, userName, password) else null
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account): LoginFragment {
|
|
val ret = LoginFragment()
|
|
ret.accountV1 = accountV1
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
class LoginDoFragment() : DialogFragment() {
|
|
private val model: ConfigurationViewModel by activityViewModels()
|
|
|
|
internal lateinit var accountV1: Account
|
|
internal lateinit var loginCredentials: LoginCredentials
|
|
|
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
val progress = ProgressDialog(activity)
|
|
progress.setTitle(R.string.setting_up_encryption)
|
|
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
|
progress.isIndeterminate = true
|
|
progress.setCanceledOnTouchOutside(false)
|
|
isCancelable = false
|
|
return progress
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
if (savedInstanceState == null) {
|
|
val settings = AccountSettings(requireContext(), accountV1)
|
|
model.login(requireContext(), loginCredentials)
|
|
model.observe(this) {
|
|
if (it.isFailed) {
|
|
reportErrorHelper(requireContext(), it.error!!)
|
|
dismissAllowingStateLoss()
|
|
} else {
|
|
doAsync {
|
|
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
|
|
val client = Client.create(httpClient, it.url.toString())
|
|
val etebase = EtebaseAccount.restore(client, it.etebaseSession!!, null)
|
|
uiThread {
|
|
fragmentManager?.commit {
|
|
replace(R.id.fragment_container, WizardCollectionsFragment.newInstance(accountV1, etebase))
|
|
addToBackStack(null)
|
|
}
|
|
dismissAllowingStateLoss()
|
|
}
|
|
}
|
|
} }
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account, loginCredentials: LoginCredentials): LoginDoFragment {
|
|
val ret = LoginDoFragment()
|
|
ret.accountV1 = accountV1
|
|
ret.loginCredentials = loginCredentials
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
class WizardCollectionsFragment() : Fragment() {
|
|
private val loadingModel: LoadingViewModel by viewModels()
|
|
private lateinit var info: AccountActivity.AccountInfo
|
|
private val migrateJournals = HashMap<String, AccountActivity.CollectionListItemInfo>()
|
|
|
|
internal lateinit var accountV1: Account
|
|
internal lateinit var etebase: EtebaseAccount
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
val ret = inflater.inflate(R.layout.migrate_v2_collections, container, false)
|
|
|
|
if (savedInstanceState == null) {
|
|
if (container != null) {
|
|
initUi(inflater, ret)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
private fun initUi(inflater: LayoutInflater, v: View) {
|
|
v.findViewById<Button>(R.id.button_create).setOnClickListener {
|
|
MigrateCollectionsDoFragment.newInstance(etebase, this.migrateJournals).show(parentFragmentManager, null)
|
|
}
|
|
|
|
v.findViewById<Button>(R.id.button_skip).setOnClickListener {
|
|
activity?.finish()
|
|
}
|
|
|
|
loadAccount(v)
|
|
}
|
|
|
|
private fun loadAccount(v: View) {
|
|
val account = accountV1
|
|
info = AccountActivity.AccountInfo()
|
|
val data = (requireContext().applicationContext as App).data
|
|
|
|
loadingModel.setLoading(true)
|
|
doAsync {
|
|
try {
|
|
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
|
|
val service = serviceEntity.type!!
|
|
when (service) {
|
|
CollectionInfo.Type.ADDRESS_BOOK -> {
|
|
info.carddav = AccountActivity.AccountInfo.ServiceInfo()
|
|
info.carddav!!.infos = getLegacyJournals(data, serviceEntity)
|
|
|
|
val accountManager = AccountManager.get(context)
|
|
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
|
val addressBook = LocalAddressBook(requireContext(), addrBookAccount, null)
|
|
try {
|
|
if (account == addressBook.mainAccount)
|
|
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
|
|
} catch (e: ContactsStorageException) {
|
|
}
|
|
|
|
}
|
|
}
|
|
CollectionInfo.Type.CALENDAR -> {
|
|
info.caldav = AccountActivity.AccountInfo.ServiceInfo()
|
|
info.caldav!!.infos = getLegacyJournals(data, serviceEntity)
|
|
}
|
|
CollectionInfo.Type.TASKS -> {
|
|
info.taskdav = AccountActivity.AccountInfo.ServiceInfo()
|
|
info.taskdav!!.infos = getLegacyJournals(data, serviceEntity)
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
uiThread {
|
|
loadingModel.setLoading(false)
|
|
}
|
|
}
|
|
|
|
uiThread {
|
|
if (info.carddav != null) {
|
|
val infos = info.carddav!!.infos!!
|
|
val listCardDAV = v.findViewById<View>(R.id.address_books) as ListView
|
|
val adapter = CollectionListAdapter(requireContext(), account)
|
|
adapter.addAll(infos)
|
|
listCardDAV.adapter = adapter
|
|
listCardDAV.setOnItemClickListener { adapterView, view, i, l ->
|
|
val infoItem = infos.get(i)
|
|
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
|
|
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
|
|
} else {
|
|
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
|
|
}
|
|
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
|
|
}
|
|
}
|
|
|
|
if (info.caldav != null) {
|
|
val infos = info.caldav!!.infos!!
|
|
val listCalDAV = v.findViewById<View>(R.id.calendars) as ListView
|
|
val adapter = CollectionListAdapter(requireContext(), account)
|
|
adapter.addAll(infos)
|
|
listCalDAV.adapter = adapter
|
|
listCalDAV.setOnItemClickListener { adapterView, view, i, l ->
|
|
val infoItem = infos.get(i)
|
|
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
|
|
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
|
|
} else {
|
|
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
|
|
}
|
|
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
|
|
}
|
|
}
|
|
|
|
if (info.taskdav != null) {
|
|
val infos = info.taskdav!!.infos!!
|
|
val listTaskDAV = v.findViewById<View>(R.id.tasklists) as ListView
|
|
val adapter = CollectionListAdapter(requireContext(), account)
|
|
adapter.addAll(infos)
|
|
listTaskDAV.adapter = adapter
|
|
listTaskDAV.setOnItemClickListener { adapterView, view, i, l ->
|
|
val infoItem = infos.get(i)
|
|
if (this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)) {
|
|
this@WizardCollectionsFragment.migrateJournals.remove(infoItem.uid)
|
|
} else {
|
|
this@WizardCollectionsFragment.migrateJournals.set(infoItem.uid, infoItem)
|
|
}
|
|
view.findViewById<CheckBox>(R.id.sync).isChecked = this@WizardCollectionsFragment.migrateJournals.contains(infoItem.uid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getLegacyJournals(data: MyEntityDataStore, serviceEntity: ServiceEntity): List<AccountActivity.CollectionListItemInfo> {
|
|
return JournalEntity.getJournals(data, serviceEntity).map {
|
|
val info = it.info
|
|
val isAdmin = it.isOwner(accountV1.name)
|
|
AccountActivity.CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description
|
|
?: "", info.color, it.isReadOnly, isAdmin, info)
|
|
}
|
|
}
|
|
|
|
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<AccountActivity.CollectionListItemInfo>(context, R.layout.account_collection_item) {
|
|
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
|
var v = _v
|
|
if (v == null)
|
|
v = LayoutInflater.from(context).inflate(R.layout.account_collection_item, parent, false)
|
|
|
|
v!!.findViewById<View>(R.id.sync).visibility = View.VISIBLE
|
|
val info = getItem(position)!!
|
|
|
|
var tv = v!!.findViewById<View>(R.id.title) as TextView
|
|
tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName
|
|
|
|
tv = v.findViewById<View>(R.id.description) as TextView
|
|
if (TextUtils.isEmpty(info.description))
|
|
tv.visibility = View.GONE
|
|
else {
|
|
tv.visibility = View.VISIBLE
|
|
tv.text = info.description
|
|
}
|
|
|
|
val vColor = v.findViewById<View>(R.id.color)
|
|
if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
|
|
vColor.visibility = View.GONE
|
|
} else {
|
|
vColor.setBackgroundColor(info.color ?: LocalCalendar.defaultColor)
|
|
}
|
|
|
|
val readOnly = v.findViewById<View>(R.id.read_only)
|
|
readOnly.visibility = if (info.isReadOnly) View.VISIBLE else View.GONE
|
|
|
|
val shared = v.findViewById<View>(R.id.shared)
|
|
val isOwner = info.isAdmin
|
|
shared.visibility = if (isOwner) View.GONE else View.VISIBLE
|
|
|
|
return v
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
fun newInstance(accountV1: Account, etebase: EtebaseAccount): WizardCollectionsFragment {
|
|
val ret = WizardCollectionsFragment()
|
|
ret.accountV1 = accountV1
|
|
ret.etebase = etebase
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class MigrateCollectionsDoFragment : DialogFragment() {
|
|
private val configurationModel: ConfigurationViewModel by activityViewModels()
|
|
private lateinit var progress: ProgressDialog
|
|
private val CHUNK_SIZE = 20
|
|
|
|
internal lateinit var etebase: EtebaseAccount
|
|
internal lateinit var migrateJournals: HashMap<String, AccountActivity.CollectionListItemInfo>
|
|
|
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
progress = ProgressDialog(activity)
|
|
progress.setTitle(R.string.migrate_v2_wizard_migrate_title)
|
|
progress.setMessage(getString(R.string.migrate_v2_wizard_migrate_title))
|
|
progress.isIndeterminate = true
|
|
progress.setCanceledOnTouchOutside(false)
|
|
isCancelable = false
|
|
return progress
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
if (savedInstanceState == null) {
|
|
migrate()
|
|
}
|
|
}
|
|
|
|
private fun migrate() {
|
|
val data = (requireContext().applicationContext as App).data
|
|
doAsync {
|
|
try {
|
|
val total = migrateJournals.size
|
|
var malformed = 0
|
|
var badMtime = 0
|
|
var i = 1
|
|
val colMgr = etebase.collectionManager
|
|
for (itemInfo in migrateJournals.values) {
|
|
uiThread {
|
|
progress.setMessage(getString(R.string.migrate_v2_wizard_migrate_progress, i, total))
|
|
}
|
|
val colType = when (itemInfo.enumType) {
|
|
CollectionInfo.Type.ADDRESS_BOOK -> Constants.ETEBASE_TYPE_ADDRESS_BOOK
|
|
CollectionInfo.Type.CALENDAR -> Constants.ETEBASE_TYPE_CALENDAR
|
|
CollectionInfo.Type.TASKS -> Constants.ETEBASE_TYPE_TASKS
|
|
}
|
|
|
|
val colMeta = ItemMetadata()
|
|
colMeta.name = itemInfo.displayName
|
|
colMeta.description = itemInfo.description
|
|
if (itemInfo.color != null) {
|
|
colMeta.color = String.format("#%06X", 0xFFFFFF and itemInfo.color)
|
|
}
|
|
colMeta.mtime = System.currentTimeMillis()
|
|
val collection = colMgr.create(colType, colMeta, "")
|
|
colMgr.upload(collection)
|
|
|
|
val migratedItems = HashMap<String, Item>()
|
|
val journalEntity = JournalModel.Journal.fetch(data, itemInfo.legacyInfo!!.getServiceEntity(data), itemInfo.uid)
|
|
val entries = data.select(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.asc()).get().toList()
|
|
val itemMgr = colMgr.getItemManager(collection)
|
|
var itemDone = 0
|
|
val toPush = LinkedList<Item>()
|
|
for (entry in entries) {
|
|
itemDone++
|
|
|
|
val inputReader = StringReader(entry.content.content)
|
|
val uid: String?
|
|
var lastModified: Long?
|
|
when (itemInfo.enumType) {
|
|
CollectionInfo.Type.ADDRESS_BOOK -> {
|
|
val contact = Contact.fromReader(inputReader, null)[0]
|
|
uid = contact.uid
|
|
lastModified = null
|
|
}
|
|
CollectionInfo.Type.CALENDAR -> {
|
|
val event = Event.eventsFromReader(inputReader)[0]
|
|
uid = event.uid
|
|
lastModified = event.lastModified?.dateTime?.time
|
|
}
|
|
CollectionInfo.Type.TASKS -> {
|
|
val task = Task.tasksFromReader(inputReader)[0]
|
|
uid = task.uid
|
|
lastModified = task.lastModified
|
|
}
|
|
}
|
|
|
|
if (uid == null) {
|
|
malformed++
|
|
continue
|
|
}
|
|
if (lastModified == null) {
|
|
// When we can't set mtime, set to the item's position in the change log so we at least maintain EteSync 1.0 ordering.
|
|
lastModified = System.currentTimeMillis() + itemDone
|
|
badMtime++
|
|
}
|
|
|
|
val item: Item
|
|
if (migratedItems.containsKey(uid)) {
|
|
val tmp = migratedItems.get(uid)!!
|
|
// We need to clone the item so we can push multiple versions at once
|
|
item = itemMgr.cacheLoad(itemMgr.cacheSaveWithContent(tmp))
|
|
item.setContent(entry.content.content)
|
|
val meta = item.meta
|
|
meta.mtime = lastModified
|
|
item.meta = meta
|
|
} else {
|
|
val meta = ItemMetadata()
|
|
meta.mtime = lastModified
|
|
meta.name = uid
|
|
item = itemMgr.create(meta, entry.content.content)
|
|
migratedItems.set(uid, item)
|
|
}
|
|
if (entry.content.isAction(SyncEntry.Actions.DELETE)) {
|
|
item.delete()
|
|
}
|
|
toPush.add(item)
|
|
|
|
if (toPush.size == CHUNK_SIZE) {
|
|
uiThread {
|
|
progress.setMessage(context?.getString(R.string.migrate_v2_wizard_migrate_progress, i, total) + "\n" +
|
|
getString(R.string.migrate_v2_wizard_migrate_progress_entries, itemDone, entries.size))
|
|
}
|
|
itemMgr.batch(toPush.toTypedArray())
|
|
toPush.clear()
|
|
}
|
|
}
|
|
if (toPush.size > 0) {
|
|
itemMgr.batch(toPush.toTypedArray())
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
uiThread {
|
|
var message = getString(R.string.migrate_v2_wizard_migrate_progress_done)
|
|
if (malformed > 0) {
|
|
message += "\n\n" + getString(R.string.migrate_v2_wizard_migrate_progress_done_malformed, malformed)
|
|
}
|
|
AlertDialog.Builder(requireContext())
|
|
.setIcon(R.drawable.ic_info_dark)
|
|
.setTitle(R.string.migrate_v2_wizard_migrate_title)
|
|
.setMessage(message)
|
|
.setPositiveButton(android.R.string.yes) { _, _ -> }
|
|
.setOnDismissListener {
|
|
requireFragmentManager().commit {
|
|
replace(android.R.id.content, CreateAccountFragment.newInstance(configurationModel.account.value!!))
|
|
addToBackStack(null)
|
|
}
|
|
dismissAllowingStateLoss()
|
|
}
|
|
.show()
|
|
}
|
|
} catch (e: Exception) {
|
|
uiThread {
|
|
reportErrorHelper(requireContext(), e)
|
|
dismissAllowingStateLoss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
companion object {
|
|
fun newInstance(etebase: EtebaseAccount,
|
|
migrateJournals: HashMap<String, AccountActivity.CollectionListItemInfo>): MigrateCollectionsDoFragment {
|
|
val ret = MigrateCollectionsDoFragment()
|
|
ret.etebase = etebase
|
|
ret.migrateJournals = migrateJournals
|
|
return ret
|
|
}
|
|
}
|
|
} |