From 6cd5a5bba64757768313aedd405540ec9b1c80cb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Aug 2020 12:42:11 +0300 Subject: [PATCH 01/83] Add etebase dep. --- app/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index ddf107f9..62d50667 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -134,6 +134,8 @@ dependencies { implementation "org.jetbrains.anko:anko-commons:0.10.4" implementation "com.etesync:journalmanager:1.1.1" + def etebaseVersion = '0.1.3-SNAPSHOT' + implementation "com.etebase:client:$etebaseVersion" def acraVersion = '5.3.0' implementation "ch.acra:acra-mail:$acraVersion" From 1062ed583360a9cd18eb670c10141c7585bb4afc Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 13:23:28 +0300 Subject: [PATCH 02/83] Update strings because usernames need not be emails anymore. --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ef2ac25..9f68c15d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,8 +207,8 @@ Add account - Email - Valid email address required + Username + Valid username required Password EteSync Server URL Invalid URL found, did you forget to include https://? From 65861b3f1c07f9b57471ccabdf149910530d152f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 13:48:22 +0300 Subject: [PATCH 03/83] Account settings: add support for storing an etebase session. --- app/src/main/java/com/etesync/syncadapter/AccountSettings.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt index 6e58ee0d..9cd4c3af 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt +++ b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt @@ -73,6 +73,9 @@ constructor(internal val context: Context, internal val account: Account) { get() = accountManager.getUserData(account, KEY_WIFI_ONLY_SSID) set(ssid) = accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid) + var etebaseSession: String? + get() = accountManager.getUserData(account, KEY_ETEBASE_SESSION) + set(value) = accountManager.setUserData(account, KEY_ETEBASE_SESSION, value) // CalDAV settings @@ -216,6 +219,7 @@ constructor(internal val context: Context, internal val account: Account) { private val KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key" private val KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key" private val KEY_WIFI_ONLY = "wifi_only" + private val KEY_ETEBASE_SESSION = "etebase_session" // sync on WiFi only (default: false) private val KEY_WIFI_ONLY_SSID = "wifi_only_ssid" // restrict sync to specific WiFi SSID From ee8c8d8fe1b0903e9572e6621445fa425333ae69 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 13:52:50 +0300 Subject: [PATCH 04/83] Login credentials: keep the default uri (even if null). We need this for etebase compat. --- .../ui/setup/BaseConfigurationFinder.kt | 11 +++++++---- .../syncadapter/ui/setup/LoginCredentials.kt | 17 +---------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt index 6785e5e6..87649439 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt @@ -13,6 +13,7 @@ import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.JournalAuthenticator import com.etesync.journalmanager.UserInfoManager +import com.etesync.syncadapter.Constants import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import okhttp3.HttpUrl @@ -36,15 +37,17 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred val cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK) val calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR) - val authenticator = JournalAuthenticator(httpClient, credentials.uri?.toHttpUrlOrNull()!!) + val uri = credentials.uri ?: URI(Constants.serviceUrl.toString()) + + val authenticator = JournalAuthenticator(httpClient, uri.toHttpUrlOrNull()!!) var authtoken: String? = null var userInfo: UserInfoManager.UserInfo? = null try { authtoken = authenticator.getAuthToken(credentials.userName, credentials.password) - val authenticatedHttpClient = HttpClient.Builder(context, credentials.uri.host, authtoken!!).build().okHttpClient - val userInfoManager = UserInfoManager(authenticatedHttpClient, credentials.uri.toHttpUrlOrNull()!!) + val authenticatedHttpClient = HttpClient.Builder(context, uri.host, authtoken!!).build().okHttpClient + val userInfoManager = UserInfoManager(authenticatedHttpClient, uri.toHttpUrlOrNull()!!) userInfo = userInfoManager.fetch(credentials.userName) } catch (e: Exceptions.HttpException) { Logger.log.warning(e.message) @@ -55,7 +58,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred } return Configuration( - credentials.uri, + uri, credentials.userName, authtoken, cardDavConfig, calDavConfig, userInfo, diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt index 47244cca..5709553c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentials.kt @@ -15,22 +15,7 @@ import com.etesync.syncadapter.log.Logger import java.net.URI import java.net.URISyntaxException -class LoginCredentials(_uri: URI?, val userName: String, val password: String) : Parcelable { - val uri: URI? - - init { - var uri = _uri - - if (uri == null) { - try { - uri = URI(Constants.serviceUrl.toString()) - } catch (e: URISyntaxException) { - Logger.log.severe("Should never happen, it's a constant") - } - - } - this.uri = uri - } +class LoginCredentials(val uri: URI?, val userName: String, val password: String) : Parcelable { override fun describeContents(): Int { return 0 From 693157f71e0466ed62f2b2e34ccb70a8864d4726 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 14:06:46 +0300 Subject: [PATCH 05/83] Fix gradle warning. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 62d50667..406088f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,9 +7,9 @@ */ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' android { compileSdkVersion rootProject.ext.compileSdkVersion From 4bf36e7ad31a84e5aea01a42c105d0fc3afe283c Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 14:13:48 +0300 Subject: [PATCH 06/83] Add kotlin extensions, enable R8 and fix errors. --- app/build.gradle | 6 +++++- .../com/etesync/syncadapter/ui/AccountListFragment.kt | 4 ++-- .../syncadapter/ui/CollectionMembersListFragment.kt | 4 ++-- .../etesync/syncadapter/ui/importlocal/ImportActivity.kt | 4 ++-- .../etesync/syncadapter/ui/importlocal/ResultFragment.kt | 4 ++-- .../syncadapter/ui/journalviewer/ListEntriesFragment.kt | 2 +- .../syncadapter/ui/setup/DetectConfigurationFragment.kt | 8 ++++---- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 406088f5..62fd2d78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,9 +143,13 @@ dependencies { def supportVersion = '1.0.0' implementation "androidx.legacy:legacy-support-core-ui:$supportVersion" implementation "androidx.core:core:$supportVersion" - implementation "androidx.fragment:fragment:$supportVersion" implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.cardview:cardview:1.0.0" + // KTX extensions + implementation "androidx.core:core-ktx:1.3.1" + implementation "androidx.fragment:fragment-ktx:1.2.5" + + implementation 'com.google.android.material:material:1.2.0-beta01' implementation "androidx.legacy:legacy-preference-v14:$supportVersion" implementation 'com.github.yukuku:ambilwarna:2.0.1' diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt index 52abf562..78c580ab 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountListFragment.kt @@ -32,7 +32,7 @@ import com.etesync.syncadapter.R class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks>, AdapterView.OnItemClickListener { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - listAdapter = AccountListAdapter(context!!) + listAdapter = AccountListAdapter(requireContext()) return inflater.inflate(R.layout.account_list, container, false) } @@ -58,7 +58,7 @@ class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks> { - return AccountLoader(context!!) + return AccountLoader(requireContext()) } override fun onLoadFinished(loader: Loader>, accounts: Array) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt index a8cc1e0e..46c50987 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.kt @@ -106,13 +106,13 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val member = listAdapter?.getItem(position) as JournalManager.Member - AlertDialog.Builder(activity!!) + AlertDialog.Builder(requireActivity()) .setIcon(R.drawable.ic_info_dark) .setTitle(R.string.collection_members_remove_title) .setMessage(getString(R.string.collection_members_remove, member.user)) .setPositiveButton(android.R.string.yes) { dialog, which -> val frag = RemoveMemberFragment.newInstance(account, info, member.user!!) - frag.show(fragmentManager!!, null) + frag.show(requireFragmentManager(), null) } .setNegativeButton(android.R.string.no) { dialog, which -> }.show() } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt index d9600cfe..f7b22533 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt @@ -108,9 +108,9 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { - mSelectImportMethod = activity as SelectImportMethod? + mSelectImportMethod = activity as SelectImportMethod } catch (e: ClassCastException) { - throw ClassCastException(activity!!.toString() + " must implement MyInterface ") + throw ClassCastException(activity.toString() + " must implement MyInterface ") } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt index 86ab9288..1971b018 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt @@ -19,7 +19,7 @@ class ResultFragment : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - result = arguments!!.getSerializable(KEY_RESULT) as ImportResult + result = requireArguments().getSerializable(KEY_RESULT) as ImportResult } override fun onDismiss(dialog: DialogInterface) { @@ -32,7 +32,7 @@ class ResultFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { if (result!!.isFailed) { - return AlertDialog.Builder(activity!!) + return AlertDialog.Builder(requireActivity()) .setTitle(R.string.import_dialog_failed_title) .setIcon(R.drawable.ic_error_dark) .setMessage(getString(R.string.import_dialog_failed_body, result!!.e!!.localizedMessage)) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt index 2efd5bde..2e4aa593 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.kt @@ -88,7 +88,7 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val entry = listAdapter?.getItem(position) as EntryEntity - startActivity(JournalItemActivity.newIntent(context!!, account, info, entry.content)) + startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content)) } internal inner class EntriesListAdapter(context: Context) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt index e0a74aa3..8dca2727 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt @@ -42,19 +42,19 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba } override fun onCreateLoader(id: Int, args: Bundle?): Loader { - return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials) + return ServerConfigurationLoader(requireContext(), args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials) } override fun onLoadFinished(loader: Loader, data: Configuration?) { if (data != null) { if (data.isFailed) // no service found: show error message - fragmentManager!!.beginTransaction() + requireFragmentManager().beginTransaction() .add(NothingDetectedFragment.newInstance(data.error!!.localizedMessage), null) .commitAllowingStateLoss() else // service found: continue - fragmentManager!!.beginTransaction() + requireFragmentManager().beginTransaction() .replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data)) .addToBackStack(null) .commitAllowingStateLoss() @@ -76,7 +76,7 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba .setMessage(R.string.login_wrong_username_or_password) .setNeutralButton(R.string.login_view_logs) { dialog, which -> val intent = DebugInfoActivity.newIntent(context, this::class.toString()) - intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS)) + intent.putExtra(DebugInfoActivity.KEY_LOGS, requireArguments().getString(KEY_LOGS)) startActivity(intent) } .setPositiveButton(android.R.string.ok) { dialog, which -> From feed7c2119ddade3cea7db73d92e382446766c99 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 14:17:41 +0300 Subject: [PATCH 07/83] Add lifecycle kotlin extensions. --- app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 62fd2d78..8537caed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -148,6 +148,9 @@ dependencies { // KTX extensions implementation "androidx.core:core-ktx:1.3.1" implementation "androidx.fragment:fragment-ktx:1.2.5" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" implementation 'com.google.android.material:material:1.2.0-beta01' From 476f756307f2dab9e654b44e8ce887b5019fa980 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 14:31:49 +0300 Subject: [PATCH 08/83] Simplify Configuration class - caldav/carddav aren't required. --- .../ui/setup/BaseConfigurationFinder.kt | 15 ++------------- .../ui/setup/SetupEncryptionFragment.kt | 9 +++++---- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt index 87649439..338c52ec 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt @@ -34,8 +34,6 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred fun findInitialConfiguration(): Configuration { var exception: Throwable? = null - val cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK) - val calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR) val uri = credentials.uri ?: URI(Constants.serviceUrl.toString()) @@ -60,26 +58,17 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred return Configuration( uri, credentials.userName, authtoken, - cardDavConfig, calDavConfig, userInfo, exception ) } - protected fun findInitialConfiguration(service: CollectionInfo.Type): Configuration.ServiceInfo { - // put discovered information here - val config = Configuration.ServiceInfo() - Logger.log.info("Finding initial " + service.toString() + " service configuration") - - return config - } - // data classes class Configuration // We have to use URI here because HttpUrl is not serializable! - (val url: URI, val userName: String, val authtoken: String?, val cardDAV: ServiceInfo, val calDAV: ServiceInfo, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable { + (val url: URI, val userName: String, val authtoken: String?, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable { var rawPassword: String? = null var password: String? = null var keyPair: Crypto.AsymmetricKeyPair? = null @@ -96,7 +85,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred } override fun toString(): String { - return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", cardDAV=" + this.cardDAV + ", calDAV=" + this.calDAV + ", error=" + this.error + ", failed=" + this.isFailed + ")" + return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", error=" + this.error + ", failed=" + this.isFailed + ")" } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt index 3c88b0d8..46204dea 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.kt @@ -134,13 +134,13 @@ class SetupEncryptionFragment : DialogFragment() { } // insert CardDAV service - insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV) + insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK) // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong()) // insert CalDAV service - insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV) + insertService(accountName, CollectionInfo.Type.CALENDAR) // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong()) @@ -160,8 +160,9 @@ class SetupEncryptionFragment : DialogFragment() { return true } - protected fun insertService(accountName: String, serviceType: CollectionInfo.Type, info: BaseConfigurationFinder.Configuration.ServiceInfo) { - val data = (context!!.applicationContext as App).data + protected fun insertService(accountName: String, serviceType: CollectionInfo.Type) { + val info = Configuration.ServiceInfo() + val data = (requireContext().applicationContext as App).data // insert service val serviceEntity = ServiceEntity.fetchOrCreate(data, accountName, serviceType) From 5da8edd54da7fa0de638949db34bd8dfcc477924 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 15:00:27 +0300 Subject: [PATCH 09/83] Login dialog: add support for login into etebase accounts --- .../etesync/syncadapter/AccountSettings.kt | 4 +- .../ui/setup/BaseConfigurationFinder.kt | 76 +++++++++++- .../ui/setup/CreateAccountFragment.kt | 111 ++++++++++++++++++ .../ui/setup/DetectConfigurationFragment.kt | 15 ++- 4 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/CreateAccountFragment.kt diff --git a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt index 9cd4c3af..4967508e 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt +++ b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt @@ -247,10 +247,10 @@ constructor(internal val context: Context, internal val account: Account) { val SYNC_INTERVAL_MANUALLY: Long = -1 // XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work. - fun setUserData(accountManager: AccountManager, account: Account, uri: URI, userName: String) { + fun setUserData(accountManager: AccountManager, account: Account, uri: URI?, userName: String) { accountManager.setUserData(account, KEY_SETTINGS_VERSION, CURRENT_VERSION.toString()) accountManager.setUserData(account, KEY_USERNAME, userName) - accountManager.setUserData(account, KEY_URI, uri.toString()) + accountManager.setUserData(account, KEY_URI, uri?.toString()) } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt index 338c52ec..c9c44d04 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt @@ -8,6 +8,9 @@ package com.etesync.syncadapter.ui.setup import android.content.Context +import com.etebase.client.Account +import com.etebase.client.Client +import com.etebase.client.exceptions.EtebaseException import com.etesync.syncadapter.HttpClient import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions @@ -19,6 +22,7 @@ import com.etesync.syncadapter.model.CollectionInfo import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient +import okhttp3.Request import java.io.IOException import java.io.Serializable import java.net.URI @@ -31,8 +35,26 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred httpClient = HttpClient.Builder(context).build().okHttpClient } + private fun isServerEtebase(): Boolean { + if (credentials.uri != null) { + val remote = credentials.uri.toHttpUrlOrNull()!!.newBuilder() + .addPathSegments("api/v1/authentication/is_etebase/") + .build() - fun findInitialConfiguration(): Configuration { + val request = Request.Builder() + .get() + .url(remote) + .build() + + val response = httpClient.newCall(request).execute() + + return response.isSuccessful + } else { + return !credentials.userName.contains("@") + } + } + + fun findInitialConfigurationLegacy(): Configuration { var exception: Throwable? = null val uri = credentials.uri ?: URI(Constants.serviceUrl.toString()) @@ -57,18 +79,63 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred return Configuration( uri, - credentials.userName, authtoken, + credentials.userName, + null, + authtoken, userInfo, exception ) } + fun findInitialConfigurationEtebase(): Configuration { + var exception: Throwable? = null + + val uri = credentials.uri + + var etebaseSession: String? = null + try { + val client = Client.create(httpClient, uri.toString()) + val etebase = Account.login(client, credentials.userName, credentials.password) + etebaseSession = etebase.save(null) + } catch (e: EtebaseException) { + exception = e + } + + return Configuration( + uri, + credentials.userName, + etebaseSession, + null, + null, + exception + ) + } + + fun findInitialConfiguration(): Configuration { + try { + if (isServerEtebase()) { + return findInitialConfigurationEtebase() + } else { + return findInitialConfigurationLegacy() + } + } catch (e: Exception) { + return Configuration( + credentials.uri, + credentials.userName, + null, + null, + null, + e + ) + } + } + // data classes class Configuration // We have to use URI here because HttpUrl is not serializable! - (val url: URI, val userName: String, val authtoken: String?, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable { + (val url: URI?, val userName: String, val etebaseSession: String?, val authtoken: String?, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable { var rawPassword: String? = null var password: String? = null var keyPair: Crypto.AsymmetricKeyPair? = null @@ -76,6 +143,9 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred val isFailed: Boolean get() = this.error != null + val isLegacy: Boolean + get() = this.authtoken != null + class ServiceInfo : Serializable { val collections: Map = HashMap() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/CreateAccountFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/CreateAccountFragment.kt new file mode 100644 index 00000000..be50ec8f --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/CreateAccountFragment.kt @@ -0,0 +1,111 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.setup + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Activity +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS +import com.etesync.journalmanager.Crypto +import com.etesync.journalmanager.Exceptions +import com.etesync.syncadapter.* +import com.etesync.syncadapter.log.Logger +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.ServiceEntity +import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration +import com.etesync.syncadapter.utils.AndroidCompat +import com.etesync.syncadapter.utils.TaskProviderHandling +import java.util.logging.Level + +class CreateAccountFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(activity) + progress.setTitle(R.string.login_encryption_setup_title) + progress.setMessage(getString(R.string.login_encryption_setup)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + return progress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val config = requireArguments().getSerializable(KEY_CONFIG) as Configuration + + val activity = requireActivity() + if (createAccount(config.userName, config)) { + activity.setResult(Activity.RESULT_OK) + activity.finish() + } + } + + @Throws(InvalidAccountException::class) + protected fun createAccount(accountName: String, config: Configuration): Boolean { + val account = Account(accountName, App.accountType) + + // create Android account + Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url)) + + val accountManager = AccountManager.get(context) + if (!accountManager.addAccountExplicitly(account, config.password, null)) + return false + + AccountSettings.setUserData(accountManager, account, config.url, config.userName) + + // add entries for account to service DB + Logger.log.log(Level.INFO, "Writing account configuration to database", config) + try { + val settings = AccountSettings(requireContext(), account) + + settings.etebaseSession = config.etebaseSession + + // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml + settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong()) + + // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml + settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong()) + + OPENTASK_PROVIDERS.forEach { + // enable task sync if OpenTasks is installed + // further changes will be handled by PackageChangedReceiver + TaskProviderHandling.updateTaskSync(requireContext(), it) + } + + } catch (e: InvalidAccountException) { + Logger.log.log(Level.SEVERE, "Couldn't access account settings", e) + AndroidCompat.removeAccount(accountManager, account) + throw e + } + + return true + } + + companion object { + private val KEY_CONFIG = "config" + + fun newInstance(config: Configuration): CreateAccountFragment { + val frag = CreateAccountFragment() + val args = Bundle(1) + args.putSerializable(KEY_CONFIG, config) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt index 8dca2727..9f7e732c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/DetectConfigurationFragment.kt @@ -13,6 +13,7 @@ import android.app.ProgressDialog import android.content.Context import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.loader.app.LoaderManager @@ -47,17 +48,23 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba override fun onLoadFinished(loader: Loader, data: Configuration?) { if (data != null) { - if (data.isFailed) - // no service found: show error message + if (data.isFailed) { + // no service found: show error message requireFragmentManager().beginTransaction() .add(NothingDetectedFragment.newInstance(data.error!!.localizedMessage), null) .commitAllowingStateLoss() - else - // service found: continue + } else if (data.isLegacy) { + // legacy service found: continue requireFragmentManager().beginTransaction() .replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data)) .addToBackStack(null) .commitAllowingStateLoss() + } else { + requireFragmentManager().beginTransaction() + .replace(android.R.id.content, CreateAccountFragment.newInstance(data)) + .addToBackStack(null) + .commitAllowingStateLoss() + } } else Logger.log.severe("Configuration detection failed") From 90cc39deebe33f7eedc5198f82ead843e5682477 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 16:08:27 +0300 Subject: [PATCH 10/83] EtebaseLocalCache: add a class that implements a local cache for etebase. --- .../etesync/syncadapter/EtebaseLocalCache.kt | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt new file mode 100644 index 00000000..0e198b09 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -0,0 +1,90 @@ +package com.etesync.syncadapter + +import android.content.Context +import com.etebase.client.* +import com.etebase.client.Collection +import java.io.File + +/* +File structure: +cache_dir/ + user1/ <--- the name of the user + cols/ + UID1/ - The uid of the first col + ... + UID2/ - The uid of the second col + col <-- the col itself + items/ + item_uid1 <-- the item with uid 1 + item_uid2 + ... + */ +class EtebaseLocalCache private constructor(context: Context, username: String) { + private val filesDir: File + private val colsDir: File + + init { + filesDir = File(context.filesDir, username) + colsDir = File(filesDir, "cols") + colsDir.mkdirs() + } + + private fun getCollectionItemsDir(colUid: String): File { + val colsDir = File(filesDir, "cols") + val colDir = File(colsDir, colUid) + return File(colDir, "items") + } + + fun clearUserCache() { + filesDir.deleteRecursively() + } + + fun collectionList(colMgr: CollectionManager): List { + return colsDir.list().map { + val colFile = File(it, "col") + val content = colFile.readBytes() + colMgr.cacheLoad(content) + } + } + + fun collectionSet(colMgr: CollectionManager, collection: Collection) { + val colDir = File(colsDir, collection.uid) + colDir.mkdir() + val colFile = File(colDir, "col") + colFile.writeBytes(colMgr.cacheSave(collection)) + val itemsDir = getCollectionItemsDir(collection.uid) + itemsDir.mkdir() + } + + fun collectionUnset(colMgr: CollectionManager, colUid: String) { + val colDir = File(colsDir, colUid) + colDir.deleteRecursively() + } + + fun itemList(itemMgr: ItemManager, colUid: String): List { + val itemsDir = getCollectionItemsDir(colUid) + return itemsDir.list().map { + val itemFile = File(it) + val content = itemFile.readBytes() + itemMgr.cacheLoad(content) + } + } + + fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { + val itemsDir = getCollectionItemsDir(colUid) + val itemFile = File(itemsDir, item.uid) + itemFile.writeBytes(itemMgr.cacheSave(item)) + } + + fun itemUnset(itemMgr: ItemManager, colUid: String, itemUid: String) { + val itemsDir = getCollectionItemsDir(colUid) + val itemFile = File(itemsDir, itemUid) + itemFile.delete() + } + + companion object { + fun getInstance(context: Context, username: String): EtebaseLocalCache { + return EtebaseLocalCache(context, username) + } + } +} From 09c932c02c20ce3bed94dd220603dc7de2ce6894 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 16:18:15 +0300 Subject: [PATCH 11/83] Account Settings: add a flag to check if legacy (and use in account page). --- .../main/java/com/etesync/syncadapter/AccountSettings.kt | 3 +++ .../java/com/etesync/syncadapter/ui/AccountActivity.kt | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt index 4967508e..99b18e2b 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt +++ b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt @@ -77,6 +77,9 @@ constructor(internal val context: Context, internal val account: Account) { get() = accountManager.getUserData(account, KEY_ETEBASE_SESSION) set(value) = accountManager.setUserData(account, KEY_ETEBASE_SESSION, value) + val isLegacy: Boolean + get() = authToken != null + // CalDAV settings var manageCalendarColors: Boolean diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 1cbf7b14..08eae1f5 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -87,6 +87,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe account = intent.getParcelableExtra(EXTRA_ACCOUNT) title = account.name + val settings = AccountSettings(this, account) setContentView(R.layout.activity_account) @@ -131,8 +132,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true) } - if (!SetupUserInfoFragment.hasUserInfo(this, account)) { - SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null) + if (settings.isLegacy) { + if (!SetupUserInfoFragment.hasUserInfo(this, account)) { + SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null) + } } } From 69b044a4448a4f438355cbd1900bdbce1edb0a38 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 16:50:27 +0300 Subject: [PATCH 12/83] Remove redundant code. --- .../main/java/com/etesync/syncadapter/App.kt | 31 ------------------- .../syncadapter/model/CollectionInfo.kt | 17 ---------- 2 files changed, 48 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/App.kt b/app/src/main/java/com/etesync/syncadapter/App.kt index c5e15239..3d125410 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.kt +++ b/app/src/main/java/com/etesync/syncadapter/App.kt @@ -158,22 +158,6 @@ class App : Application() { private fun update(fromVersion: Int) { Logger.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE) - if (fromVersion < 6) { - val data = this.data - - val dbHelper = ServiceDB.OpenHelper(this) - - val collections = readCollections(dbHelper) - for (info in collections) { - val journalEntity = JournalEntity(data, info) - data.insert(journalEntity) - } - - val db = dbHelper.writableDatabase - db.delete(ServiceDB.Collections._TABLE, null, null) - db.close() - } - if (fromVersion < 7) { /* Fix all of the etags to be non-null */ val am = AccountManager.get(this) @@ -234,21 +218,6 @@ class App : Application() { } - private fun readCollections(dbHelper: ServiceDB.OpenHelper): List { - val db = dbHelper.writableDatabase - val collections = LinkedList() - val cursor = db.query(ServiceDB.Collections._TABLE, null, null, null, null, null, null) - while (cursor.moveToNext()) { - val values = ContentValues() - DatabaseUtils.cursorRowToContentValues(cursor, values) - collections.add(CollectionInfo.fromDB(values)) - } - - db.close() - cursor.close() - return collections - } - fun migrateServices(dbHelper: ServiceDB.OpenHelper) { val db = dbHelper.readableDatabase val data = this.data diff --git a/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.kt b/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.kt index 72fbf6ad..9fd97fd6 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.kt +++ b/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.kt @@ -48,23 +48,6 @@ class CollectionInfo : com.etesync.journalmanager.model.CollectionInfo() { return info } - fun fromDB(values: ContentValues): CollectionInfo { - val info = CollectionInfo() - info.id = values.getAsLong(Collections.ID)!! - info.serviceID = values.getAsInteger(Collections.SERVICE_ID)!! - - info.uid = values.getAsString(Collections.URL) - info.displayName = values.getAsString(Collections.DISPLAY_NAME) - info.description = values.getAsString(Collections.DESCRIPTION) - - info.color = values.getAsInteger(Collections.COLOR) - - info.timeZone = values.getAsString(Collections.TIME_ZONE) - - info.selected = values.getAsInteger(Collections.SYNC) != 0 - return info - } - fun fromJson(json: String): CollectionInfo { return GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, CollectionInfo::class.java) } From eeb93f523d576e88580b222857fe73ff3cb9b1a0 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 18:08:59 +0300 Subject: [PATCH 13/83] Get account list: add etebase support. --- .../com/etesync/syncadapter/Constants.java | 4 + .../etesync/syncadapter/EtebaseLocalCache.kt | 6 + .../syncadapter/resource/LocalCalendar.kt | 2 +- .../etesync/syncadapter/ui/AccountActivity.kt | 165 ++++++++++++------ 4 files changed, 125 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/Constants.java b/app/src/main/java/com/etesync/syncadapter/Constants.java index 04dedb82..af2bf909 100644 --- a/app/src/main/java/com/etesync/syncadapter/Constants.java +++ b/app/src/main/java/com/etesync/syncadapter/Constants.java @@ -43,4 +43,8 @@ public class Constants { public final static String KEY_ACCOUNT = "account", KEY_COLLECTION_INFO = "collectionInfo"; + + public final static String ETEBASE_TYPE_ADDRESS_BOOK = "etebase.vcard"; + public final static String ETEBASE_TYPE_CALENDAR = "etebase.vevent"; + public final static String ETEBASE_TYPE_TASKS = "etebase.vtodo"; } diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index 0e198b09..6c1477dd 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -86,5 +86,11 @@ class EtebaseLocalCache private constructor(context: Context, username: String) fun getInstance(context: Context, username: String): EtebaseLocalCache { return EtebaseLocalCache(context, username) } + + fun getEtebase(context: Context, settings: AccountSettings): Account { + val httpClient = HttpClient.Builder(context).build().okHttpClient + val client = Client.create(httpClient, settings.uri?.toString()) + return Account.restore(client, settings.etebaseSession!!, null) + } } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index e72862a3..9808a0f7 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -31,7 +31,7 @@ class LocalCalendar private constructor( ): AndroidCalendar(account, provider, LocalEvent.Factory, id), LocalCollection { companion object { - val defaultColor = -0x743cb6 // light green 500 + val defaultColor = -0x743cb6 // light green 500 - should be "8BC349"? val COLUMN_CTAG = Calendars.CAL_SYNC1 diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 08eae1f5..0afb770b 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -13,6 +13,7 @@ import android.accounts.AccountManager import android.app.LoaderManager import android.content.* import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE +import android.graphics.Color.parseColor import android.net.Uri import android.os.Build import android.os.Bundle @@ -25,16 +26,18 @@ import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat -import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.CollectionManager import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.JournalAuthenticator +import com.etesync.syncadapter.Constants.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.MyEntityDataStore import com.etesync.syncadapter.model.ServiceEntity import com.etesync.syncadapter.resource.LocalAddressBook import com.etesync.syncadapter.resource.LocalCalendar @@ -52,6 +55,7 @@ import java.util.logging.Level class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks, Refreshable { private lateinit var account: Account + private lateinit var settings: AccountSettings private var accountInfo: AccountInfo? = null internal var listCalDAV: ListView? = null @@ -64,10 +68,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe private val onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, _ -> val list = parent as ListView val adapter = list.adapter as ArrayAdapter<*> - val journalEntity = adapter.getItem(position) as JournalEntity - val info = journalEntity.getInfo() + val info = adapter.getItem(position) as CollectionListItemInfo - startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info)) + startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!)) } private val formattedFingerprint: String? @@ -87,7 +90,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe account = intent.getParcelableExtra(EXTRA_ACCOUNT) title = account.name - val settings = AccountSettings(this, account) + settings = AccountSettings(this, account) setContentView(R.layout.activity_account) @@ -230,10 +233,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe internal var taskdav: ServiceInfo? = null class ServiceInfo { - internal var id: Long = 0 internal var refreshing: Boolean = false - internal var journals: List? = null + internal var infos: List? = null } } @@ -257,7 +259,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe listCardDAV!!.setAlpha(if (info.carddav!!.refreshing) 0.5f else 1f) val adapter = CollectionListAdapter(this, account) - adapter.addAll(info.carddav!!.journals!!) + adapter.addAll(info.carddav!!.infos!!) listCardDAV!!.adapter = adapter listCardDAV!!.onItemClickListener = onItemClickListener } @@ -271,7 +273,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe listCalDAV!!.setAlpha(if (info.caldav!!.refreshing) 0.5f else 1f) val adapter = CollectionListAdapter(this, account) - adapter.addAll(info.caldav!!.journals!!) + adapter.addAll(info.caldav!!.infos!!) listCalDAV!!.adapter = adapter listCalDAV!!.onItemClickListener = onItemClickListener } @@ -285,7 +287,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe listTaskDAV!!.setAlpha(if (info.taskdav!!.refreshing) 0.5f else 1f) val adapter = CollectionListAdapter(this, account) - adapter.addAll(info.taskdav!!.journals!!) + adapter.addAll(info.taskdav!!.infos!!) listTaskDAV!!.adapter = adapter listTaskDAV!!.onItemClickListener = onItemClickListener @@ -345,50 +347,110 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe forceLoad() } + private fun getLegacyJournals(data: MyEntityDataStore, serviceEntity: ServiceEntity): List { + return JournalEntity.getJournals(data, serviceEntity).map { + val info = it.info + val isAdmin = it.isOwner(account.name) + CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description ?: "", info.color, it.isReadOnly, isAdmin, info) + } + } + + private fun getCollections(etebaseLocalCache: EtebaseLocalCache, colMgr: CollectionManager, type: CollectionInfo.Type): List { + val strType = when (type) { + CollectionInfo.Type.ADDRESS_BOOK -> ETEBASE_TYPE_ADDRESS_BOOK + CollectionInfo.Type.CALENDAR -> ETEBASE_TYPE_CALENDAR + CollectionInfo.Type.TASKS -> ETEBASE_TYPE_TASKS + } + + return etebaseLocalCache.collectionList(colMgr).map { + val meta = it.meta + + if (strType != meta.collectionType) { + return@map null + } + + val accessLevel = it.accessLevel + val isReadOnly = accessLevel == "ro" + val isAdmin = accessLevel == "adm" + + CollectionListItemInfo(it.uid, type, meta.name, meta.description ?: "", parseColor(meta.color), isReadOnly, isAdmin, null) + }.filterNotNull() + } + override fun loadInBackground(): AccountInfo { val info = AccountInfo() + val settings = AccountSettings(context, account) + if (settings.isLegacy) { + val data = (context.applicationContext as App).data + + for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { + val id = serviceEntity.id.toLong() + val service = serviceEntity.type!! + when (service) { + CollectionInfo.Type.ADDRESS_BOOK -> { + info.carddav = AccountInfo.ServiceInfo() + info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority) + info.carddav!!.infos = getLegacyJournals(data, serviceEntity) + + val accountManager = AccountManager.get(context) + for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { + val addressBook = LocalAddressBook(context, addrBookAccount, null) + try { + if (account == addressBook.mainAccount) + info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY) + } catch (e: ContactsStorageException) { + } - val data = (context.applicationContext as App).data - - for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { - val id = serviceEntity.id.toLong() - val service = serviceEntity.type!! - when (service) { - CollectionInfo.Type.ADDRESS_BOOK -> { - info.carddav = AccountInfo.ServiceInfo() - info.carddav!!.id = id - info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority) - info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity) - - val accountManager = AccountManager.get(context) - for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { - val addressBook = LocalAddressBook(context, 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 = AccountInfo.ServiceInfo() - info.caldav!!.id = id - info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) || - ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) - info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity) - } - CollectionInfo.Type.TASKS -> { - info.taskdav = AccountInfo.ServiceInfo() - info.taskdav!!.id = id - info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) || - OPENTASK_PROVIDERS.any { - ContentResolver.isSyncActive(account, it.authority) - } - info.taskdav!!.journals = JournalEntity.getJournals(data, serviceEntity) + CollectionInfo.Type.CALENDAR -> { + info.caldav = AccountInfo.ServiceInfo() + info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) || + ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) + info.caldav!!.infos = getLegacyJournals(data, serviceEntity) + } + CollectionInfo.Type.TASKS -> { + info.taskdav = AccountInfo.ServiceInfo() + info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) || + OPENTASK_PROVIDERS.any { + ContentResolver.isSyncActive(account, it.authority) + } + info.taskdav!!.infos = getLegacyJournals(data, serviceEntity) + } } } + return info } + + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + val etebase = EtebaseLocalCache.getEtebase(context, settings) + val colMgr = etebase.collectionManager + + info.carddav = AccountInfo.ServiceInfo() + info.carddav!!.refreshing = ContentResolver.isSyncActive(account, App.addressBooksAuthority) + info.carddav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS) + + val accountManager = AccountManager.get(context) + for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { + val addressBook = LocalAddressBook(context, addrBookAccount, null) + try { + if (account == addressBook.mainAccount) + info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY) + } catch (e: ContactsStorageException) { + } + + } + + info.caldav = AccountInfo.ServiceInfo() + info.caldav!!.refreshing = ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) + info.caldav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS) + + info.taskdav = AccountInfo.ServiceInfo() + info.taskdav!!.refreshing = OPENTASK_PROVIDERS.any { + ContentResolver.isSyncActive(account, it.authority) + } + info.taskdav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS) + return info } } @@ -396,15 +458,16 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe /* LIST ADAPTERS */ - class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter(context, R.layout.account_collection_item) { + data class CollectionListItemInfo(val uid: String, val enumType: CollectionInfo.Type, val displayName: String, val description: String, val color: Int?, val isReadOnly: Boolean, val isAdmin: Boolean, val legacyInfo: CollectionInfo?) + + class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter(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) - val journalEntity = getItem(position) - val info = journalEntity!!.info + val info = getItem(position)!! var tv = v!!.findViewById(R.id.title) as TextView tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName @@ -425,10 +488,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe } val readOnly = v.findViewById(R.id.read_only) - readOnly.visibility = if (journalEntity.isReadOnly) View.VISIBLE else View.GONE + readOnly.visibility = if (info.isReadOnly) View.VISIBLE else View.GONE val shared = v.findViewById(R.id.shared) - val isOwner = journalEntity.isOwner(account.name) + val isOwner = info.isAdmin shared.visibility = if (isOwner) View.GONE else View.VISIBLE return v From 79b650da380fb7b58adb44003dde695dde530fd7 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 18:21:30 +0300 Subject: [PATCH 14/83] LocalEtebaseCache: make sure we always return the same item. --- .../com/etesync/syncadapter/EtebaseLocalCache.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index 6c1477dd..f619fea8 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -4,6 +4,7 @@ import android.content.Context import com.etebase.client.* import com.etebase.client.Collection import java.io.File +import java.util.HashMap /* File structure: @@ -83,8 +84,19 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } companion object { + private val localCacheCache: HashMap = HashMap() + fun getInstance(context: Context, username: String): EtebaseLocalCache { - return EtebaseLocalCache(context, username) + synchronized(localCacheCache) { + val cached = localCacheCache.get(username) + if (cached != null) { + return cached + } else { + val ret = EtebaseLocalCache(context, username) + localCacheCache.set(username, ret) + return ret + } + } } fun getEtebase(context: Context, settings: AccountSettings): Account { From 2c0e14d3a3486fccfdadcc799cfd715f8fcd8a76 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 18:45:36 +0300 Subject: [PATCH 15/83] EtebaseLocalCache: add stoken and fix issues with loading files. --- .../etesync/syncadapter/EtebaseLocalCache.kt | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index f619fea8..c214a090 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -3,13 +3,15 @@ package com.etesync.syncadapter import android.content.Context import com.etebase.client.* import com.etebase.client.Collection +import okhttp3.OkHttpClient import java.io.File -import java.util.HashMap +import java.util.* /* File structure: cache_dir/ user1/ <--- the name of the user + stoken cols/ UID1/ - The uid of the first col ... @@ -21,11 +23,10 @@ cache_dir/ ... */ class EtebaseLocalCache private constructor(context: Context, username: String) { - private val filesDir: File + private val filesDir: File = File(context.filesDir, username) private val colsDir: File init { - filesDir = File(context.filesDir, username) colsDir = File(filesDir, "cols") colsDir.mkdirs() } @@ -40,9 +41,20 @@ class EtebaseLocalCache private constructor(context: Context, username: String) filesDir.deleteRecursively() } + fun saveStoken(stoken: String) { + val stokenFile = File(filesDir, "stoken") + stokenFile.writeText(stoken) + } + + fun loadStoken(): String? { + val stokenFile = File(filesDir, "stoken") + return if (stokenFile.exists()) stokenFile.readText() else null + } + fun collectionList(colMgr: CollectionManager): List { return colsDir.list().map { - val colFile = File(it, "col") + val colDir = File(colsDir, it) + val colFile = File(colDir, "col") val content = colFile.readBytes() colMgr.cacheLoad(content) } @@ -52,7 +64,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) val colDir = File(colsDir, collection.uid) colDir.mkdir() val colFile = File(colDir, "col") - colFile.writeBytes(colMgr.cacheSave(collection)) + colFile.writeBytes(colMgr.cacheSaveWithContent(collection)) val itemsDir = getCollectionItemsDir(collection.uid) itemsDir.mkdir() } @@ -65,7 +77,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) fun itemList(itemMgr: ItemManager, colUid: String): List { val itemsDir = getCollectionItemsDir(colUid) return itemsDir.list().map { - val itemFile = File(it) + val itemFile = File(itemsDir, it) val content = itemFile.readBytes() itemMgr.cacheLoad(content) } @@ -74,7 +86,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { val itemsDir = getCollectionItemsDir(colUid) val itemFile = File(itemsDir, item.uid) - itemFile.writeBytes(itemMgr.cacheSave(item)) + itemFile.writeBytes(itemMgr.cacheSaveWithContent(item)) } fun itemUnset(itemMgr: ItemManager, colUid: String, itemUid: String) { @@ -99,8 +111,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } - fun getEtebase(context: Context, settings: AccountSettings): Account { - val httpClient = HttpClient.Builder(context).build().okHttpClient + fun getEtebase(context: Context, httpClient: OkHttpClient, settings: AccountSettings): Account { val client = Client.create(httpClient, settings.uri?.toString()) return Account.restore(client, settings.etebaseSession!!, null) } From 608f1ff37180c57d423b5609774aaecd68f73c14 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 18:51:57 +0300 Subject: [PATCH 16/83] Sync collections and show them in the account page. --- .../etesync/syncadapter/EtebaseLocalCache.kt | 8 +-- .../syncadapter/SyncAdapterService.kt | 66 ++++++++++++++----- .../etesync/syncadapter/ui/AccountActivity.kt | 11 ++-- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index c214a090..8eb762d9 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -51,13 +51,13 @@ class EtebaseLocalCache private constructor(context: Context, username: String) return if (stokenFile.exists()) stokenFile.readText() else null } - fun collectionList(colMgr: CollectionManager): List { + fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List { return colsDir.list().map { val colDir = File(colsDir, it) val colFile = File(colDir, "col") val content = colFile.readBytes() colMgr.cacheLoad(content) - } + }.filter { withDeleted || !it.isDeleted } } fun collectionSet(colMgr: CollectionManager, collection: Collection) { @@ -74,13 +74,13 @@ class EtebaseLocalCache private constructor(context: Context, username: String) colDir.deleteRecursively() } - fun itemList(itemMgr: ItemManager, colUid: String): List { + fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List { val itemsDir = getCollectionItemsDir(colUid) return itemsDir.list().map { val itemFile = File(itemsDir, it) val content = itemFile.readBytes() itemMgr.cacheLoad(content) - } + }.filter { withDeleted || !it.isDeleted } } fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt index 45c228d2..ecc1bd6e 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt @@ -22,6 +22,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.util.Pair import at.bitfire.ical4android.CalendarStorageException import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.FetchOptions import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions @@ -208,30 +209,59 @@ abstract class SyncAdapterService : Service() { val settings = AccountSettings(context, account) val httpClient = HttpClient.Builder(context, settings).setForeground(false).build() - val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!) - - var journals = journalFetcher.list(journalsManager, settings, serviceType) - - if (journals.isEmpty()) { - journals = LinkedList() - try { - val info = CollectionInfo.defaultForServiceType(serviceType) - val uid = JournalManager.Journal.genUid() - info.uid = uid - val crypto = Crypto.CryptoManager(info.version, settings.password(), uid) - val journal = JournalManager.Journal(crypto, info.toJson(), uid) - journalsManager.create(journal) - journals.add(Pair(journal, info)) - } catch (e: Exceptions.AssociateNotAllowedException) { - // Skip for now + if (settings.isLegacy) { + val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!) + + var journals = journalFetcher.list(journalsManager, settings, serviceType) + + if (journals.isEmpty()) { + journals = LinkedList() + try { + val info = CollectionInfo.defaultForServiceType(serviceType) + val uid = JournalManager.Journal.genUid() + info.uid = uid + val crypto = Crypto.CryptoManager(info.version, settings.password(), uid) + val journal = JournalManager.Journal(crypto, info.toJson(), uid) + journalsManager.create(journal) + journals.add(Pair(journal, info)) + } catch (e: Exceptions.AssociateNotAllowedException) { + // Skip for now + } + } + + legacySaveCollections(journals) + + httpClient.close() + return + } + + + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + synchronized(etebaseLocalCache) { + val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) + val colMgr = etebase.collectionManager + var stoken = etebaseLocalCache.loadStoken() + var done = false + while (!done) { + val colList = colMgr.list(FetchOptions().stoken(stoken)) + for (col in colList.data) { + etebaseLocalCache.collectionSet(colMgr, col) + } + + for (col in colList.removedMemberships) { + etebaseLocalCache.collectionUnset(colMgr, col.uid()) + } + + stoken = colList.stoken + done = colList.isDone + etebaseLocalCache.saveStoken(stoken!!) } } - saveCollections(journals) httpClient.close() } - private fun saveCollections(journals: Iterable>) { + private fun legacySaveCollections(journals: Iterable>) { val data = (context.applicationContext as App).data val service = JournalModel.Service.fetchOrCreate(data, account.name, serviceType) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 0afb770b..cfcde47a 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -373,7 +373,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe val isReadOnly = accessLevel == "ro" val isAdmin = accessLevel == "adm" - CollectionListItemInfo(it.uid, type, meta.name, meta.description ?: "", parseColor(meta.color), isReadOnly, isAdmin, null) + val metaColor = meta.color + val color = if (metaColor != null && metaColor != "") parseColor(metaColor) else null + CollectionListItemInfo(it.uid, type, meta.name, meta.description ?: "", color, isReadOnly, isAdmin, null) }.filterNotNull() } @@ -423,12 +425,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe } val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) - val etebase = EtebaseLocalCache.getEtebase(context, settings) + val httpClient = HttpClient.Builder(context).build().okHttpClient + val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings) val colMgr = etebase.collectionManager info.carddav = AccountInfo.ServiceInfo() info.carddav!!.refreshing = ContentResolver.isSyncActive(account, App.addressBooksAuthority) - info.carddav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS) + info.carddav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.ADDRESS_BOOK) val accountManager = AccountManager.get(context) for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { @@ -443,7 +446,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe info.caldav = AccountInfo.ServiceInfo() info.caldav!!.refreshing = ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) - info.caldav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS) + info.caldav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.CALENDAR) info.taskdav = AccountInfo.ServiceInfo() info.taskdav!!.refreshing = OPENTASK_PROVIDERS.any { From 2069e9b215bec7a170128cab0fef22914cfb3658 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 07:31:01 +0300 Subject: [PATCH 17/83] LocalEtebaseCache: create a shared collection + meta type. --- .../com/etesync/syncadapter/EtebaseLocalCache.kt | 16 ++++++++++++---- .../etesync/syncadapter/ui/AccountActivity.kt | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index 8eb762d9..d5068eec 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -51,13 +51,15 @@ class EtebaseLocalCache private constructor(context: Context, username: String) return if (stokenFile.exists()) stokenFile.readText() else null } - fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List { + fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List { return colsDir.list().map { val colDir = File(colsDir, it) val colFile = File(colDir, "col") val content = colFile.readBytes() colMgr.cacheLoad(content) - }.filter { withDeleted || !it.isDeleted } + }.filter { withDeleted || !it.isDeleted }.map{ + CachedCollection(it, it.meta) + } } fun collectionSet(colMgr: CollectionManager, collection: Collection) { @@ -74,13 +76,15 @@ class EtebaseLocalCache private constructor(context: Context, username: String) colDir.deleteRecursively() } - fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List { + fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List { val itemsDir = getCollectionItemsDir(colUid) return itemsDir.list().map { val itemFile = File(itemsDir, it) val content = itemFile.readBytes() itemMgr.cacheLoad(content) - }.filter { withDeleted || !it.isDeleted } + }.filter { withDeleted || !it.isDeleted }.map { + CachedItem(it, it.meta) + } } fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { @@ -117,3 +121,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } } + +data class CachedCollection(val col: Collection, val meta: CollectionMetadata) + +data class CachedItem(val item: Item, val meta: ItemMetadata) \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index cfcde47a..819dda45 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -369,13 +369,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe return@map null } - val accessLevel = it.accessLevel + val accessLevel = it.col.accessLevel val isReadOnly = accessLevel == "ro" val isAdmin = accessLevel == "adm" val metaColor = meta.color val color = if (metaColor != null && metaColor != "") parseColor(metaColor) else null - CollectionListItemInfo(it.uid, type, meta.name, meta.description ?: "", color, isReadOnly, isAdmin, null) + CollectionListItemInfo(it.col.uid, type, meta.name, meta.description ?: "", color, isReadOnly, isAdmin, null) }.filterNotNull() } From 1c284bce915a9f5a52f7baff6a3f52b3f00c5f97 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 07:34:51 +0300 Subject: [PATCH 18/83] CalendarSyncAdapter: implement syncing etebase calendars. --- .../syncadapter/resource/LocalCalendar.kt | 44 ++++++++++++++ .../CalendarsSyncAdapterService.kt | 57 +++++++++++++++++-- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 9808a0f7..e4d99a0a 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -13,11 +13,13 @@ import android.content.ContentProviderClient import android.content.ContentProviderOperation import android.content.ContentUris import android.content.ContentValues +import android.graphics.Color.parseColor import android.net.Uri import android.os.RemoteException import android.provider.CalendarContract import android.provider.CalendarContract.* import at.bitfire.ical4android.* +import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.JournalEntity import org.apache.commons.lang3.StringUtils @@ -50,6 +52,21 @@ class LocalCalendar private constructor( return AndroidCalendar.create(account, provider, values) } + fun create(account: Account, provider: ContentProviderClient, cachedCollection: CachedCollection): Uri { + val values = valuesFromCachedCollection(cachedCollection, true) + + // ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash. + values.put(Calendars.ACCOUNT_NAME, account.name) + values.put(Calendars.ACCOUNT_TYPE, account.type) + values.put(Calendars.OWNER_ACCOUNT, account.name) + + // flag as visible & synchronizable at creation, might be changed by user at any time + values.put(Calendars.VISIBLE, 1) + values.put(Calendars.SYNC_EVENTS, 1) + + return AndroidCalendar.create(account, provider, values) + } + fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar? = AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull() @@ -85,6 +102,30 @@ class LocalCalendar private constructor( values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", ")) return values } + + private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues { + val values = ContentValues() + val col = cachedCollection.col + val meta = cachedCollection.meta + values.put(Calendars.NAME, col.uid) + values.put(Calendars.CALENDAR_DISPLAY_NAME, meta.name) + + if (withColor) + values.put(Calendars.CALENDAR_COLOR, if (!meta.color.isNullOrBlank()) parseColor(meta.color) else defaultColor) + + if (col.accessLevel == "ro") + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) + else { + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER) + values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1) + values.put(Calendars.CAN_ORGANIZER_RESPOND, 1) + } + + values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT) + values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ",")) + values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", ")) + return values + } } override val url: String? @@ -93,6 +134,9 @@ class LocalCalendar private constructor( fun update(journalEntity: JournalEntity, updateColor: Boolean) = update(valuesFromCollectionInfo(journalEntity, updateColor)) + fun update(cachedCollection: CachedCollection, updateColor: Boolean) = + update(valuesFromCachedCollection(cachedCollection, updateColor)) + override fun findDeleted() = queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt index c377a886..e6c6de03 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.kt @@ -13,10 +13,7 @@ import android.os.Bundle import android.provider.CalendarContract import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.CalendarStorageException -import com.etesync.syncadapter.AccountSettings -import com.etesync.syncadapter.App -import com.etesync.syncadapter.Constants -import com.etesync.syncadapter.R +import com.etesync.syncadapter.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity @@ -42,7 +39,11 @@ class CalendarsSyncAdapterService : SyncAdapterService() { RefreshCollections(account, CollectionInfo.Type.CALENDAR).run() - updateLocalCalendars(provider, account, settings) + if (settings.isLegacy) { + legacyUpdateLocalCalendars(provider, account, settings) + } else { + updateLocalCalendars(provider, account, settings) + } val principal = settings.uri?.toHttpUrlOrNull()!! @@ -56,8 +57,52 @@ class CalendarsSyncAdapterService : SyncAdapterService() { Logger.log.info("Calendar sync complete") } - @Throws(CalendarStorageException::class) private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) { + val remote = HashMap() + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + val collections: List + synchronized(etebaseLocalCache) { + val httpClient = HttpClient.Builder(context, settings).setForeground(false).build() + val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) + val colMgr = etebase.collectionManager + + collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR } + } + + for (collection in collections) { + remote[collection.col.uid] = collection + } + + val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null) + + val updateColors = settings.manageCalendarColors + + // delete obsolete local calendar + for (calendar in local) { + val url = calendar.name + val collection = remote[url] + if (collection == null) { + Logger.log.fine("Deleting obsolete local calendar $url") + calendar.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.fine("Updating local calendar $url") + calendar.update(collection, updateColors) + // we already have a local calendar for this remote collection, don't take into consideration anymore + remote.remove(url) + } + } + + // create new local calendars + for (url in remote.keys) { + val cachedCollection = remote[url]!! + Logger.log.info("Adding local calendar list $cachedCollection") + LocalCalendar.create(account, provider, cachedCollection) + } + } + + @Throws(CalendarStorageException::class) + private fun legacyUpdateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) { val data = (context.applicationContext as App).data val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.CALENDAR) From d6a0958d1674b8e98380d08ebfcea9ff05fd9118 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 07:49:00 +0300 Subject: [PATCH 19/83] TaskListSyncAdapter: implement syncing etebase task lists --- .../syncadapter/resource/LocalTaskList.kt | 26 +++++++++ .../syncadapter/TasksSyncAdapterService.kt | 56 +++++++++++++++++-- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index e061781b..686da075 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -11,14 +11,17 @@ package com.etesync.syncadapter.resource import android.accounts.Account import android.content.ContentValues import android.content.Context +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.RemoteException +import android.provider.CalendarContract import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.AndroidTaskListFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.ProviderName +import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.model.JournalEntity import org.dmfs.tasks.contract.TaskContract.TaskLists import org.dmfs.tasks.contract.TaskContract.Tasks @@ -54,6 +57,14 @@ class LocalTaskList private constructor( return create(account, provider, values) } + fun create(account: Account, provider: TaskProvider, cachedCollection: CachedCollection): Uri { + val values = valuesFromCachedCollection(cachedCollection, true) + values.put(TaskLists.OWNER, account.name) + values.put(TaskLists.SYNC_ENABLED, 1) + values.put(TaskLists.VISIBLE, 1) + return create(account, provider, values) + } + fun findByName(account: Account, provider: TaskProvider, factory: Factory, name: String): LocalTaskList? = AndroidTaskList.find(account, provider, factory, TaskLists._SYNC_ID + "==?", arrayOf(name)).firstOrNull() @@ -70,6 +81,18 @@ class LocalTaskList private constructor( return values } + private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues { + val col = cachedCollection.col + val meta = cachedCollection.meta + val values = ContentValues(3) + values.put(TaskLists._SYNC_ID, col.uid) + values.put(TaskLists.LIST_NAME, meta.name) + + if (withColor) + values.put(TaskLists.LIST_COLOR, if (!meta.color.isNullOrBlank()) Color.parseColor(meta.color) else defaultColor) + + return values + } } override val url: String? @@ -78,6 +101,9 @@ class LocalTaskList private constructor( fun update(journalEntity: JournalEntity, updateColor: Boolean) = update(valuesFromCollectionInfo(journalEntity, updateColor)) + fun update(cachedCollection: CachedCollection, updateColor: Boolean) = + update(valuesFromCachedCollection(cachedCollection, updateColor)) + override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null) override fun findDirty(limit: Int?): List { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt index 313153c0..e1b31e9e 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt @@ -18,10 +18,7 @@ import android.os.Bundle import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.ProviderName -import com.etesync.syncadapter.AccountSettings -import com.etesync.syncadapter.App -import com.etesync.syncadapter.Constants -import com.etesync.syncadapter.R +import com.etesync.syncadapter.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity @@ -63,8 +60,11 @@ class TasksSyncAdapterService: SyncAdapterService() { RefreshCollections(account, CollectionInfo.Type.TASKS).run() - updateLocalTaskLists(taskProvider, account, accountSettings) - + if (accountSettings.isLegacy) { + legacyUpdateLocalTaskLists(taskProvider, account, accountSettings) + } else { + updateLocalTaskLists(taskProvider, account, accountSettings) + } val principal = accountSettings.uri?.toHttpUrlOrNull()!! for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) { @@ -78,6 +78,50 @@ class TasksSyncAdapterService: SyncAdapterService() { } private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) { + val remote = HashMap() + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + val collections: List + synchronized(etebaseLocalCache) { + val httpClient = HttpClient.Builder(context, settings).setForeground(false).build() + val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) + val colMgr = etebase.collectionManager + + collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_TASKS } + } + + for (collection in collections) { + remote[collection.col.uid] = collection + } + + val local = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null) + + val updateColors = settings.manageCalendarColors + + // delete obsolete local calendar + for (taskList in local) { + val url = taskList.name + val collection = remote[url] + if (collection == null) { + Logger.log.fine("Deleting obsolete local taskList $url") + taskList.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.fine("Updating local taskList $url") + taskList.update(collection, updateColors) + // we already have a local taskList for this remote collection, don't take into consideration anymore + remote.remove(url) + } + } + + // create new local calendars + for (url in remote.keys) { + val cachedCollection = remote[url]!! + Logger.log.info("Adding local calendar list $cachedCollection") + LocalTaskList.create(account, provider, cachedCollection) + } + } + + private fun legacyUpdateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) { val data = (context.applicationContext as App).data var service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.TASKS) From deb1bb831b56e1d81f1a33f230798b2b990259da Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 08:02:44 +0300 Subject: [PATCH 20/83] AddressBooksSyncAdapter: implement syncing etebase address books --- .../syncadapter/resource/LocalAddressBook.kt | 74 +++++++++++++++++++ .../AddressBooksSyncAdapterService.kt | 56 ++++++++++++-- 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index a7cf378e..f288e4dd 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -20,6 +20,7 @@ import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import at.bitfire.vcard4android.* import com.etesync.syncadapter.App +import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity @@ -70,6 +71,35 @@ class LocalAddressBook( return addressBook } + fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, cachedCollection: CachedCollection): LocalAddressBook { + val col = cachedCollection.col + val accountManager = AccountManager.get(context) + + val account = Account(accountName(mainAccount, cachedCollection), App.addressBookAccountType) + val userData = initialUserData(mainAccount, col.uid) + Logger.log.log(Level.INFO, "Creating local address book $account", userData) + if (!accountManager.addAccountExplicitly(account, null, userData)) + throw IllegalStateException("Couldn't create address book account") + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + // Android < 7 seems to lose the initial user data sometimes, so set it a second time + // https://forums.bitfire.at/post/11644 + userData.keySet().forEach { key -> + accountManager.setUserData(account, key, userData.getString(key)) + } + } + + + val addressBook = LocalAddressBook(context, account, provider) + ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true) + + val values = ContentValues(2) + values.put(ContactsContract.Settings.SHOULD_SYNC, 1) + values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) + addressBook.settings = values + + return addressBook + } fun find(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context) .getAccountsByType(App.addressBookAccountType) @@ -103,6 +133,19 @@ class LocalAddressBook( return sb.toString() } + fun accountName(mainAccount: Account, cachedCollection: CachedCollection): String { + val col = cachedCollection.col + val meta = cachedCollection.meta + val displayName = meta.name + val sb = StringBuilder(displayName) + sb.append(" (") + .append(mainAccount.name) + .append(" ") + .append(col.uid.substring(0, 4)) + .append(")") + return sb.toString() + } + fun initialUserData(mainAccount: Account, url: String): Bundle { val bundle = Bundle(3) bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name) @@ -181,6 +224,37 @@ class LocalAddressBook( ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true) } + + fun update(cachedCollection: CachedCollection) { + val col = cachedCollection.col + val newAccountName = accountName(mainAccount, cachedCollection) + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) { + val accountManager = AccountManager.get(context) + val future = accountManager.renameAccount(account, newAccountName, { + try { + // update raw contacts to new account name + if (provider != null) { + val values = ContentValues(1) + values.put(RawContacts.ACCOUNT_NAME, newAccountName) + provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", + arrayOf(account.name, account.type)) + } + } catch (e: RemoteException) { + Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e) + } + }, null) + account = future.result + } + + readOnly = col.accessLevel == "ro" + Logger.log.info("Address book write permission? = ${!readOnly}") + + // make sure it will still be synchronized when contacts are updated + ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true) + } + fun delete() { val accountManager = AccountManager.get(context) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt index 9233eaab..2508a621 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt @@ -15,10 +15,7 @@ import android.content.* import android.os.Bundle import android.provider.ContactsContract import at.bitfire.vcard4android.ContactsStorageException -import com.etesync.syncadapter.AccountSettings -import com.etesync.syncadapter.App -import com.etesync.syncadapter.Constants -import com.etesync.syncadapter.R +import com.etesync.syncadapter.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity @@ -53,7 +50,11 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run() - updateLocalAddressBooks(contactsProvider, account) + if (settings.isLegacy) { + legacyUpdateLocalAddressBooks(contactsProvider, account) + } else { + updateLocalAddressBooks(contactsProvider, account, settings) + } contactsProvider.release() @@ -69,9 +70,52 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { Logger.log.info("Address book sync complete") } + private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, settings: AccountSettings) { + val remote = HashMap() + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + val collections: List + synchronized(etebaseLocalCache) { + val httpClient = HttpClient.Builder(context, settings).setForeground(false).build() + val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) + val colMgr = etebase.collectionManager + + collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK } + } + + for (collection in collections) { + remote[collection.col.uid] = collection + } + + val local = LocalAddressBook.find(context, provider, account) + + val updateColors = settings.manageCalendarColors + + // delete obsolete local calendar + for (addressBook in local) { + val url = addressBook.url + val collection = remote[url] + if (collection == null) { + Logger.log.fine("Deleting obsolete local addressBook $url") + addressBook.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.fine("Updating local addressBook $url") + addressBook.update(collection) + // we already have a local addressBook for this remote collection, don't take into consideration anymore + remote.remove(url) + } + } + + // create new local calendars + for (url in remote.keys) { + val cachedCollection = remote[url]!! + Logger.log.info("Adding local calendar list $cachedCollection") + LocalAddressBook.create(context, provider, account, cachedCollection) + } + } @Throws(ContactsStorageException::class, AuthenticatorException::class, OperationCanceledException::class, IOException::class) - private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) { + private fun legacyUpdateLocalAddressBooks(provider: ContentProviderClient, account: Account) { val context = context val data = (getContext().applicationContext as App).data val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.ADDRESS_BOOK) From ea0f97408614d4add5cfe7f6d62413b50cab185e Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 08:20:39 +0300 Subject: [PATCH 21/83] Account activity: lock the cache when using it and cleanup color parsing --- .../etesync/syncadapter/ui/AccountActivity.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 819dda45..9bf4d69e 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -362,21 +362,24 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe CollectionInfo.Type.TASKS -> ETEBASE_TYPE_TASKS } - return etebaseLocalCache.collectionList(colMgr).map { - val meta = it.meta + synchronized(etebaseLocalCache) { + return etebaseLocalCache.collectionList(colMgr).map { + val meta = it.meta - if (strType != meta.collectionType) { - return@map null - } + if (strType != meta.collectionType) { + return@map null + } - val accessLevel = it.col.accessLevel - val isReadOnly = accessLevel == "ro" - val isAdmin = accessLevel == "adm" + val accessLevel = it.col.accessLevel + val isReadOnly = accessLevel == "ro" + val isAdmin = accessLevel == "adm" - val metaColor = meta.color - val color = if (metaColor != null && metaColor != "") parseColor(metaColor) else null - CollectionListItemInfo(it.col.uid, type, meta.name, meta.description ?: "", color, isReadOnly, isAdmin, null) - }.filterNotNull() + val metaColor = meta.color + val color = if (!metaColor.isNullOrBlank()) parseColor(metaColor) else null + CollectionListItemInfo(it.col.uid, type, meta.name, meta.description + ?: "", color, isReadOnly, isAdmin, null) + }.filterNotNull() + } } override fun loadInBackground(): AccountInfo { From 85fd9fdd7cbaae13846f0328c403d2e0923bb0df Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 10:13:56 +0300 Subject: [PATCH 22/83] AccountSettings: fix issue when saved uri is null. --- .../main/java/com/etesync/syncadapter/AccountSettings.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt index 99b18e2b..1d65376c 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt +++ b/app/src/main/java/com/etesync/syncadapter/AccountSettings.kt @@ -36,8 +36,12 @@ constructor(internal val context: Context, internal val account: Account) { var uri: URI? get() { + val uri = accountManager.getUserData(account, KEY_URI) + if (uri == null) { + return null + } try { - return URI(accountManager.getUserData(account, KEY_URI)) + return URI(uri) } catch (e: URISyntaxException) { return null } From efdce8c557622178c0da242e72555a2e60ee11c2 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 10:22:04 +0300 Subject: [PATCH 23/83] Account: logout and clear cache on account removal. --- .../etesync/syncadapter/ui/AccountActivity.kt | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 9bf4d69e..3998271f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import at.bitfire.vcard4android.ContactsStorageException import com.etebase.client.CollectionManager +import com.etebase.client.exceptions.EtebaseException import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions @@ -509,17 +510,32 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe private fun deleteAccount() { val accountManager = AccountManager.get(this) val settings = AccountSettings(this@AccountActivity, account) - val authToken = settings.authToken - val principal = settings.uri?.toHttpUrlOrNull() doAsync { - try { - val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient - val journalAuthenticator = JournalAuthenticator(httpClient, principal!!) - journalAuthenticator.invalidateAuthToken(authToken) - } catch (e: Exceptions.HttpException) { - // Ignore failures for now - Logger.log.warning(e.toString()) + if (settings.isLegacy) { + val authToken = settings.authToken + val principal = settings.uri?.toHttpUrlOrNull() + + try { + val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient + val journalAuthenticator = JournalAuthenticator(httpClient, principal!!) + journalAuthenticator.invalidateAuthToken(authToken) + } catch (e: Exceptions.HttpException) { + // Ignore failures for now + Logger.log.warning(e.toString()) + } + } else { + val etebaseLocalCache = EtebaseLocalCache.getInstance(this@AccountActivity, account.name) + etebaseLocalCache.clearUserCache() + + try { + val httpClient = HttpClient.Builder(this@AccountActivity).build() + val etebase = EtebaseLocalCache.getEtebase(this@AccountActivity, httpClient.okHttpClient, settings) + etebase.logout() + } catch(e: EtebaseException) { + // Ignore failures for now + Logger.log.warning(e.toString()) + } } } From 6302ab42de93620009b2a362dd4e1c4a08027816 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 08:44:33 +0300 Subject: [PATCH 24/83] Sync manager: add etebase support (pulling changes) --- .../etesync/syncadapter/EtebaseLocalCache.kt | 25 +- .../syncadapter/resource/LocalAddressBook.kt | 2 +- .../syncadapter/resource/LocalCalendar.kt | 2 +- .../syncadapter/resource/LocalCollection.kt | 2 +- .../syncadapter/resource/LocalGroup.kt | 2 +- .../syncadapter/resource/LocalTaskList.kt | 2 +- .../AddressBooksSyncAdapterService.kt | 2 - .../syncadapter/CalendarSyncManager.kt | 55 ++++- .../syncadapter/ContactsSyncManager.kt | 97 +++++++- .../syncadapter/syncadapter/SyncManager.kt | 231 ++++++++++++------ .../syncadapter/TasksSyncManager.kt | 55 ++++- .../syncadapter/ui/JournalItemActivity.kt | 6 +- .../ui/importlocal/ImportFragment.kt | 9 +- .../LocalCalendarImportFragment.kt | 2 +- .../importlocal/LocalContactImportFragment.kt | 4 +- 15 files changed, 385 insertions(+), 111 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index d5068eec..5e94ff3c 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -11,12 +11,13 @@ import java.util.* File structure: cache_dir/ user1/ <--- the name of the user - stoken + stoken <-- the stokens of the collection fetch cols/ UID1/ - The uid of the first col ... UID2/ - The uid of the second col col <-- the col itself + stoken <-- the stoken of the items fetch items/ item_uid1 <-- the item with uid 1 item_uid2 @@ -51,6 +52,19 @@ class EtebaseLocalCache private constructor(context: Context, username: String) return if (stokenFile.exists()) stokenFile.readText() else null } + + fun collectionSaveStoken(colUid: String, stoken: String) { + val colDir = File(colsDir, colUid) + val stokenFile = File(colDir, "stoken") + stokenFile.writeText(stoken) + } + + fun collectionLoadStoken(colUid: String): String? { + val colDir = File(colsDir, colUid) + val stokenFile = File(colDir, "stoken") + return if (stokenFile.exists()) stokenFile.readText() else null + } + fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List { return colsDir.list().map { val colDir = File(colsDir, it) @@ -62,6 +76,15 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } + fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection { + val colDir = File(colsDir, colUid) + val colFile = File(colDir, "col") + val content = colFile.readBytes() + return colMgr.cacheLoad(content).let { + CachedCollection(it, it.meta) + } + } + fun collectionSet(colMgr: CollectionManager, collection: Collection) { val colDir = File(colsDir, collection.uid) colDir.mkdir() diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index f288e4dd..b33919b6 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -344,7 +344,7 @@ class LocalAddressBook( return reallyDirty } - override fun findByUid(uid: String): LocalAddress? { + override fun findByFilename(uid: String): LocalAddress? { val found = findContactByUID(uid) if (found != null) { return found diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index e4d99a0a..8ce27a0d 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -165,7 +165,7 @@ class LocalCalendar private constructor( override fun findAll(): List = queryEvents(null, null) - override fun findByUid(uid: String): LocalEvent? + override fun findByFilename(uid: String): LocalEvent? = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() fun processDirtyExceptions() { diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt index 8e9d7a45..5a9b5340 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -16,7 +16,7 @@ interface LocalCollection> { fun findWithoutFileName(): List fun findAll(): List - fun findByUid(uid: String): T? + fun findByFilename(uid: String): T? fun count(): Long diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index b9ffaaa0..e0ff5cf6 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -63,7 +63,7 @@ class LocalGroup : AndroidGroup, LocalAddress { // insert memberships val membersIds = members.map {uid -> Constants.log.fine("Assigning member: $uid") - val contact = addressBook.findByUid(uid) as LocalContact? + val contact = addressBook.findByFilename(uid) as LocalContact? if (contact != null) contact.id else null }.filterNotNull() diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index 686da075..dd716adb 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -126,7 +126,7 @@ class LocalTaskList private constructor( override fun findWithoutFileName(): List = queryTasks(Tasks._SYNC_ID + " IS NULL", null) - override fun findByUid(uid: String): LocalTask? + override fun findByFilename(uid: String): LocalTask? = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() override fun count(): Long { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt index 2508a621..db7bc054 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt @@ -88,8 +88,6 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { val local = LocalAddressBook.find(context, provider, account) - val updateColors = settings.manageCalendarColors - // delete obsolete local calendar for (addressBook in local) { val url = addressBook.url diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index b65b942a..6d0a760b 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -17,6 +17,7 @@ import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.Event import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.Item import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R @@ -59,7 +60,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra if (!super.prepare()) return false - journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!) + if (isLegacy) { + journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!) + } return true } @@ -77,6 +80,32 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra return localCollection as LocalCalendar } + override fun processItem(item: Item) { + val local = localCollection!!.findByFilename(item.uid) + + if (!item.isDeleted) { + val inputReader = StringReader(String(item.content)) + + val events = Event.eventsFromReader(inputReader) + if (events.size == 0) { + Logger.log.warning("Received VCard without data, ignoring") + return + } else if (events.size > 1) { + Logger.log.warning("Received multiple VCALs, using first one") + } + + val event = events[0] + processEvent(item, event, local) + } else { + if (local != null) { + Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server") + local.delete() + } else { + Logger.log.warning("Tried deleting a non-existent record: " + item.uid) + } + } + } + @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class) override fun processSyncEntryImpl(cEntry: SyncEntry) { val inputReader = StringReader(cEntry.content) @@ -90,10 +119,10 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } val event = events[0] - val local = localCollection!!.findByUid(event.uid!!) + val local = localCollection!!.findByFilename(event.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { - processEvent(event, local) + legacyProcessEvent(event, local) } else { if (local != null) { Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server") @@ -138,8 +167,26 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } } + private fun processEvent(item: Item, newData: Event, _localEvent: LocalEvent?): LocalEvent { + var localEvent = _localEvent + // delete local event, if it exists + if (localEvent != null) { + Logger.log.info("Updating " + newData.uid + " in local calendar") + localEvent.eTag = item.etag + localEvent.update(newData) + syncResult.stats.numUpdates++ + } else { + Logger.log.info("Adding " + newData.uid + " to local calendar") + localEvent = LocalEvent(localCalendar(), newData, item.uid, item.etag) + localEvent.add() + syncResult.stats.numInserts++ + } + + return localEvent + } + @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class) - private fun processEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent { + private fun legacyProcessEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent { var localEvent = _localEvent // delete local event, if it exists if (localEvent != null) { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt index 8bc2a875..ee17b13f 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt @@ -13,12 +13,14 @@ import android.content.* import android.os.Bundle import android.provider.ContactsContract import at.bitfire.ical4android.CalendarStorageException +import at.bitfire.ical4android.Event import at.bitfire.vcard4android.BatchOperation import at.bitfire.vcard4android.Contact import at.bitfire.vcard4android.ContactsStorageException import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.JournalEntryManager import com.etesync.journalmanager.model.SyncEntry +import com.etebase.client.Item import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.Constants import com.etesync.syncadapter.HttpClient @@ -77,7 +79,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } } - journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url) + if (isLegacy) { + journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url) + } return true } @@ -127,6 +131,34 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra return localCollection as LocalAddressBook } + override fun processItem(item: Item) { + val uid = item.meta.name!! + + val local = localCollection!!.findByFilename(uid) + + if (!item.isDeleted) { + val inputReader = StringReader(String(item.content)) + + val contacts = Contact.fromReader(inputReader, resourceDownloader) + if (contacts.size == 0) { + Logger.log.warning("Received VCard without data, ignoring") + return + } else if (contacts.size > 1) { + Logger.log.warning("Received multiple VCALs, using first one") + } + + val contact = contacts[0] + processContact(item, contact, local) + } else { + if (local != null) { + Logger.log.info("Removing local record which has been deleted on the server") + local.delete() + } else { + Logger.log.warning("Tried deleting a non-existent record: " + uid) + } + } + } + @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class) override fun processSyncEntryImpl(cEntry: SyncEntry) { val inputReader = StringReader(cEntry.content) @@ -139,10 +171,10 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra Logger.log.warning("Received multiple VCards, using first one") val contact = contacts[0] - val local = localCollection!!.findByUid(contact.uid!!) + val local = localCollection!!.findByFilename(contact.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { - processContact(contact, local) + legacyProcessContact(contact, local) } else { if (local != null) { Logger.log.info("Removing local record which has been deleted on the server") @@ -153,8 +185,65 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } } + private fun processContact(item: Item, newData: Contact, _local: LocalAddress?): LocalAddress { + var local = _local + val uuid = newData.uid + // update local contact, if it exists + if (local != null) { + Logger.log.log(Level.INFO, "Updating $uuid in local address book") + + if (local is LocalGroup && newData.group) { + // update group + val group: LocalGroup = local + group.eTag = item.etag + group.update(newData) + syncResult.stats.numUpdates++ + + } else if (local is LocalContact && !newData.group) { + // update contact + val contact: LocalContact = local + contact.eTag = item.etag + contact.update(newData) + syncResult.stats.numUpdates++ + + } else { + // group has become an individual contact or vice versa + try { + local.delete() + local = null + } catch (e: CalendarStorageException) { + // CalendarStorageException is not used by LocalGroup and LocalContact + } + + } + } + + if (local == null) { + if (newData.group) { + Logger.log.log(Level.INFO, "Creating local group", item.uid) + val group = LocalGroup(localAddressBook(), newData, item.uid, item.etag) + group.add() + + local = group + } else { + Logger.log.log(Level.INFO, "Creating local contact", item.uid) + val contact = LocalContact(localAddressBook(), newData, item.uid, item.etag) + contact.add() + + local = contact + } + syncResult.stats.numInserts++ + } + + if (LocalContact.HASH_HACK && local is LocalContact) + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + local.updateHashCode(null) + + return local + } + @Throws(IOException::class, ContactsStorageException::class) - private fun processContact(newData: Contact, _local: LocalAddress?): LocalAddress { + private fun legacyProcessContact(newData: Contact, _local: LocalAddress?): LocalAddress { var local = _local val uuid = newData.uid // update local contact, if it exists diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index b6f230e0..4879da4e 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -16,6 +16,7 @@ import android.os.Bundle import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.* import com.etesync.syncadapter.* import com.etesync.syncadapter.Constants.KEY_ACCOUNT import com.etesync.journalmanager.Crypto @@ -25,6 +26,8 @@ import com.etesync.journalmanager.model.SyncEntry import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.* import com.etesync.journalmanager.model.SyncEntry.Actions.ADD +import com.etesync.syncadapter.HttpClient +import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.* import com.etesync.syncadapter.ui.AccountsActivity import com.etesync.syncadapter.ui.DebugInfoActivity @@ -41,21 +44,30 @@ import kotlin.concurrent.withLock abstract class SyncManager> @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class) constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String): Closeable { + // FIXME: remove all of the lateinit once we remove legacy (and make immutable) + // RemoteEntries and the likes are probably also just relevant for legacy + protected val isLegacy: Boolean = settings.isLegacy protected val notificationManager: SyncNotification - protected val info: CollectionInfo + protected lateinit var info: CollectionInfo protected var localCollection: LocalCollection? = null protected var httpClient: HttpClient + protected lateinit var etebaseLocalCache: EtebaseLocalCache + protected lateinit var etebase: com.etebase.client.Account + protected lateinit var colMgr: CollectionManager + protected lateinit var itemMgr: ItemManager + protected lateinit var cachedCollection: CachedCollection + protected var journal: JournalEntryManager? = null private var _journalEntity: JournalEntity? = null private var numDiscarded = 0 - private val crypto: Crypto.CryptoManager + private lateinit var crypto: Crypto.CryptoManager - private val data: MyEntityDataStore + private lateinit var data: MyEntityDataStore /** * remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works. @@ -89,21 +101,31 @@ constructor(protected val context: Context, protected val account: Account, prot // create HttpClient with given logger httpClient = HttpClient.Builder(context, settings).setForeground(false).build() - data = (context.applicationContext as App).data - val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType) - info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info + if (isLegacy) { + data = (context.applicationContext as App).data + val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType) + info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info - // dismiss previous error notifications - notificationManager = SyncNotification(context, journalUid, notificationId()) - notificationManager.cancel() + Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version)) - Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version)) - - if (journalEntity.encryptedKey != null) { - crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey) + if (journalEntity.encryptedKey != null) { + crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey) + } else { + crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!) + } } else { - crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!) + etebaseLocalCache = EtebaseLocalCache.getInstance(context, accountName) + etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) + colMgr = etebase.collectionManager + synchronized(etebaseLocalCache) { + cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid) + } + itemMgr = colMgr.getItemManager(cachedCollection.col) } + + // dismiss previous error notifications + notificationManager = SyncNotification(context, journalUid, notificationId()) + notificationManager.cancel() } protected abstract fun notificationId(): Int @@ -128,48 +150,78 @@ constructor(protected val context: Context, protected val account: Account, prot Logger.log.info("Sync phase: " + context.getString(syncPhase)) prepareFetch() - do { - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_fetch_entries - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - fetchEntries() - - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_apply_remote_entries - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - applyRemoteEntries() - } while (remoteEntries!!.size == MAX_FETCH) - - do { - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_prepare_local - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - prepareLocal() - - /* Create journal entries out of local changes. */ - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_create_local_entries - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - createLocalEntries() - - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_apply_local_entries - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - /* FIXME: Skipping this now, because we already override with remote. - applyLocalEntries(); - */ - - if (Thread.interrupted()) - throw InterruptedException() - syncPhase = R.string.sync_phase_push_entries - Logger.log.info("Sync phase: " + context.getString(syncPhase)) - pushEntries() - } while (localEntries!!.size == MAX_PUSH) + if (isLegacy) { + do { + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_fetch_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + fetchEntries() + + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_apply_remote_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + applyRemoteEntries() + } while (remoteEntries!!.size == MAX_FETCH) + + do { + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_prepare_local + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + prepareLocal() + + /* Create journal entries out of local changes. */ + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_create_local_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + createLocalEntries() + + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_apply_local_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + /* FIXME: Skipping this now, because we already override with remote. + applyLocalEntries(); + */ + + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_push_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + pushEntries() + } while (localEntries!!.size == MAX_PUSH) + } else { + var itemList: ItemListResponse? + var stoken = synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid) + } + // Push local changes + + do { + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_fetch_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + itemList = fetchItems(stoken) + if (itemList == null) { + break + } + + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_apply_remote_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + applyRemoteItems(itemList) + + stoken = itemList.stoken + synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionSaveStoken(cachedCollection.col.uid, stoken!!) + } + } while (!itemList!!.isDone) + } /* Cleanup and finalize changes */ if (Thread.interrupted()) @@ -241,7 +293,8 @@ constructor(protected val context: Context, protected val account: Account, prot private fun notifyUserOnSync() { val changeNotification = context.defaultSharedPreferences.getBoolean(App.CHANGE_NOTIFICATION, true) - if (remoteEntries!!.isEmpty() || !changeNotification) { + val remoteEntries = remoteEntries + if ((remoteEntries == null) || remoteEntries.isEmpty() || !changeNotification) { return } val notificationHelper = SyncNotification(context, @@ -250,7 +303,7 @@ constructor(protected val context: Context, protected val account: Account, prot var deleted = 0 var added = 0 var changed = 0 - for (entry in remoteEntries!!) { + for (entry in remoteEntries) { val cEntry = SyncEntry.fromJournalEntry(crypto, entry) val action = cEntry.action when (action) { @@ -287,6 +340,14 @@ constructor(protected val context: Context, protected val account: Account, prot return true } + protected abstract fun processItem(item: Item) + + private fun persistItem(item: Item) { + synchronized(etebaseLocalCache) { + etebaseLocalCache.itemSet(itemMgr, cachedCollection.col.uid, item) + } + } + @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class) protected abstract fun processSyncEntryImpl(cEntry: SyncEntry) @@ -319,36 +380,46 @@ constructor(protected val context: Context, protected val account: Account, prot } } - @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.HttpException::class, InvalidCalendarException::class, InterruptedException::class) - protected fun applyLocalEntries() { - // FIXME: Need a better strategy - // We re-apply local entries so our changes override whatever was written in the remote. - val strTotal = localEntries!!.size.toString() + @Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class) + protected fun prepareFetch() { + if (isLegacy) { + remoteCTag = journalEntity.getLastUid(data) + } else { + remoteCTag = cachedCollection.col.stoken + } + } + + private fun fetchItems(stoken: String?): ItemListResponse? { + if (remoteCTag != stoken) { + val ret = itemMgr.list(FetchOptions().stoken(stoken)) + Logger.log.info("Fetched items. Done=${ret.isDone}") + return ret + } else { + Logger.log.info("Skipping fetch because local lastUid == remoteLastUid (${remoteCTag})") + return null + } + } + + private fun applyRemoteItems(itemList: ItemListResponse) { + val items = itemList.data + // Process new vcards from server + val size = items.size var i = 0 - for (entry in localEntries!!) { + for (item in items) { if (Thread.interrupted()) { throw InterruptedException() } i++ - Logger.log.info("Processing (" + i.toString() + "/" + strTotal + ") " + entry.toString()) + Logger.log.info("Processing (${i}/${size}) UID=${item.uid} Etag=${item.etag}") - val cEntry = SyncEntry.fromJournalEntry(crypto, entry) - if (cEntry.isAction(SyncEntry.Actions.DELETE)) { - continue - } - Logger.log.info("Processing resource for journal entry") - processSyncEntry(cEntry) + processItem(item) + persistItem(item) } } - @Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class) - protected fun prepareFetch() { - remoteCTag = journalEntity.getLastUid(data) - } - @Throws(Exceptions.HttpException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.IntegrityException::class) - protected fun fetchEntries() { + private fun fetchEntries() { val count = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value() if (remoteCTag != null && count == 0) { // If we are updating an existing installation with no saved journal, we need to add @@ -377,7 +448,7 @@ constructor(protected val context: Context, protected val account: Account, prot } @Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class, InterruptedException::class) - protected fun applyRemoteEntries() { + private fun applyRemoteEntries() { // Process new vcards from server val strTotal = remoteEntries!!.size.toString() var i = 0 @@ -406,7 +477,7 @@ constructor(protected val context: Context, protected val account: Account, prot } @Throws(Exceptions.HttpException::class, IOException::class, ContactsStorageException::class, CalendarStorageException::class) - protected fun pushEntries() { + private fun pushEntries() { // upload dirty contacts var pushed = 0 // FIXME: Deal with failure (someone else uploaded before we go here) @@ -510,7 +581,7 @@ constructor(protected val context: Context, protected val account: Account, prot /** */ @Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class) - protected fun prepareLocal() { + private fun prepareLocal() { localDeleted = processLocallyDeleted() localDirty = localCollection!!.findDirty(MAX_PUSH) // This is done after fetching the local dirty so all the ones we are using will be prepared diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt index 70de515e..f0b1a8de 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt @@ -13,6 +13,7 @@ import android.content.Context import android.content.SyncResult import android.os.Bundle import at.bitfire.ical4android.Task +import com.etebase.client.Item import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R @@ -58,7 +59,9 @@ class TasksSyncManager( if (!super.prepare()) return false - journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!) + if (isLegacy) { + journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!) + } return true } @@ -68,6 +71,32 @@ class TasksSyncManager( return localCollection as LocalTaskList } + override fun processItem(item: Item) { + val local = localCollection!!.findByFilename(item.uid) + + if (!item.isDeleted) { + val inputReader = StringReader(String(item.content)) + + val tasks = Task.tasksFromReader(inputReader) + if (tasks.size == 0) { + Logger.log.warning("Received VCard without data, ignoring") + return + } else if (tasks.size > 1) { + Logger.log.warning("Received multiple VCALs, using first one") + } + + val task = tasks[0] + processTask(item, task, local) + } else { + if (local != null) { + Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server") + local.delete() + } else { + Logger.log.warning("Tried deleting a non-existent record: " + item.uid) + } + } + } + override fun processSyncEntryImpl(cEntry: SyncEntry) { val inputReader = StringReader(cEntry.content) @@ -80,10 +109,10 @@ class TasksSyncManager( } val event = tasks[0] - val local = localCollection!!.findByUid(event.uid!!) + val local = localCollection!!.findByFilename(event.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { - processTask(event, local) + legacyProcessTask(event, local) } else { if (local != null) { Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server") @@ -94,7 +123,25 @@ class TasksSyncManager( } } - private fun processTask(newData: Task, _localTask: LocalTask?): LocalTask { + private fun processTask(item: Item, newData: Task, _localTask: LocalTask?): LocalTask { + var localTask = _localTask + // delete local Task, if it exists + if (localTask != null) { + Logger.log.info("Updating " + item.uid + " in local calendar") + localTask.eTag = item.etag + localTask.update(newData) + syncResult.stats.numUpdates++ + } else { + Logger.log.info("Adding " + item.uid + " to local calendar") + localTask = LocalTask(localTaskList(), newData, item.uid, item.etag) + localTask.add() + syncResult.stats.numInserts++ + } + + return localTask + } + + private fun legacyProcessTask(newData: Task, _localTask: LocalTask?): LocalTask { var localTask = _localTask // delete local Task, if it exists if (localTask != null) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt index 2f4a18bc..0a6fc2c6 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt @@ -108,7 +108,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { val provider = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!! val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)!! val event = Event.eventsFromReader(StringReader(syncEntry.content))[0] - var localEvent = localCalendar.findByUid(event.uid!!) + var localEvent = localCalendar.findByFilename(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event) } else { @@ -121,7 +121,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { val provider = TaskProvider.acquire(this, it)!! val localTaskList = LocalTaskList.findByName(account, provider, LocalTaskList.Factory, info.uid!!)!! val task = Task.tasksFromReader(StringReader(syncEntry.content))[0] - var localTask = localTaskList.findByUid(task.uid!!) + var localTask = localTaskList.findByFilename(task.uid!!) if (localTask != null) { localTask.updateAsDirty(task) } else { @@ -137,7 +137,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { if (contact.group) { // FIXME: not currently supported } else { - var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact? + var localContact = localAddressBook.findByFilename(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) } else { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt index 94d12318..5decaa76 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt @@ -15,7 +15,6 @@ import android.provider.CalendarContract import android.provider.ContactsContract import androidx.fragment.app.DialogFragment import at.bitfire.ical4android.* -import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import at.bitfire.vcard4android.BatchOperation import at.bitfire.vcard4android.Contact import at.bitfire.vcard4android.ContactsStorageException @@ -255,7 +254,7 @@ class ImportFragment : DialogFragment() { for (event in events) { try { - var localEvent = localCalendar.findByUid(event.uid!!) + var localEvent = localCalendar.findByFilename(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event) result.updated++ @@ -309,7 +308,7 @@ class ImportFragment : DialogFragment() { for (task in tasks) { try { - var localTask = localTaskList.findByUid(task.uid!!) + var localTask = localTaskList.findByFilename(task.uid!!) if (localTask != null) { localTask.updateAsDirty(task) result.updated++ @@ -353,7 +352,7 @@ class ImportFragment : DialogFragment() { for (contact in contacts.filter { contact -> !contact.group }) { try { - var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact? + var localContact = localAddressBook.findByFilename(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) @@ -386,7 +385,7 @@ class ImportFragment : DialogFragment() { } val group = contact - var localGroup: LocalGroup? = localAddressBook.findByUid(group.uid!!) as LocalGroup? + var localGroup: LocalGroup? = localAddressBook.findByFilename(group.uid!!) as LocalGroup? if (localGroup != null) { localGroup.updateAsDirty(group, memberIds) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt index c740b8ea..6218dca8 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt @@ -222,7 +222,7 @@ class LocalCalendarImportFragment : ListFragment() { var localEvent = if (event == null || event.uid == null) null else - localCalendar.findByUid(event.uid!!) + localCalendar.findByFilename(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event!!) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt index 1f0451be..079e0318 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -158,7 +158,7 @@ class LocalContactImportFragment : Fragment() { var localContact: LocalContact? = if (contact.uid == null) null else - addressBook.findByUid(contact.uid!!) as LocalContact? + addressBook.findByFilename(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) @@ -189,7 +189,7 @@ class LocalContactImportFragment : Fragment() { var localGroup: LocalGroup? = if (group.uid == null) null else - addressBook.findByUid(group.uid!!) as LocalGroup? + addressBook.findByFilename(group.uid!!) as LocalGroup? if (localGroup != null) { localGroup.updateAsDirty(group, members) From f8c0eaca357bf195486a4f4cf50ca81210764aeb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 10:57:35 +0300 Subject: [PATCH 25/83] Sync manager: add etebase support (pushing changes) --- .../etesync/syncadapter/EtebaseLocalCache.kt | 17 +- .../syncadapter/resource/LocalContact.kt | 15 +- .../syncadapter/resource/LocalEvent.kt | 18 +- .../syncadapter/resource/LocalGroup.kt | 13 +- .../syncadapter/resource/LocalResource.kt | 6 +- .../etesync/syncadapter/resource/LocalTask.kt | 17 +- .../syncadapter/CalendarSyncManager.kt | 4 +- .../syncadapter/syncadapter/SyncManager.kt | 173 ++++++++++++++++-- 8 files changed, 221 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index 5e94ff3c..83af0643 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -76,9 +76,12 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } - fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection { + fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection? { val colDir = File(colsDir, colUid) val colFile = File(colDir, "col") + if (!colFile.exists()) { + return null + } val content = colFile.readBytes() return colMgr.cacheLoad(content).let { CachedCollection(it, it.meta) @@ -110,6 +113,18 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } + fun itemGet(itemMgr: ItemManager, colUid: String, itemUid: String): CachedItem? { + val itemsDir = getCollectionItemsDir(colUid) + val itemFile = File(itemsDir, itemUid) + if (!itemFile.exists()) { + return null + } + val content = itemFile.readBytes() + return itemMgr.cacheLoad(content).let { + CachedItem(it, it.meta) + } + } + fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { val itemsDir = getCollectionItemsDir(colUid) val itemFile = File(itemsDir, item.uid) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt index 2306c948..797eb1b5 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -52,7 +52,7 @@ class LocalContact : AndroidContact, LocalAddress { override// The same now val uuid: String? - get() = fileName + get() = contact?.uid override val isLocalOnly: Boolean get() = TextUtils.isEmpty(eTag) @@ -88,9 +88,11 @@ class LocalContact : AndroidContact, LocalAddress { addressBook.provider?.update(rawContactSyncURI(), values, null, null) } - override fun clearDirty(eTag: String) { + override fun clearDirty(eTag: String?) { val values = ContentValues(3) - values.put(AndroidContact.COLUMN_ETAG, eTag) + if (eTag != null) { + values.put(AndroidContact.COLUMN_ETAG, eTag) + } values.put(ContactsContract.RawContacts.DIRTY, 0) if (LocalContact.HASH_HACK) { @@ -105,15 +107,16 @@ class LocalContact : AndroidContact, LocalAddress { this.eTag = eTag } - override fun prepareForUpload() { + override fun prepareForUpload(fileName_: String?) { val uid = UUID.randomUUID().toString() val values = ContentValues(2) - values.put(AndroidContact.COLUMN_FILENAME, uid) + val fileName = fileName_ ?: uid + values.put(AndroidContact.COLUMN_FILENAME, fileName) values.put(AndroidContact.COLUMN_UID, uid) addressBook.provider?.update(rawContactSyncURI(), values, null, null) - fileName = uid + this.fileName = fileName } override fun populateData(mimeType: String, row: ContentValues) { diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt index 810db86e..ef39ec2e 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -38,7 +38,7 @@ class LocalEvent : AndroidEvent, LocalResource { private var saveAsDirty = false // When true, the resource will be saved as dirty - private var fileName: String? = null + override var fileName: String? = null var eTag: String? = null var weAreOrganizer = true @@ -58,7 +58,7 @@ class LocalEvent : AndroidEvent, LocalResource { override// Now the same val uuid: String? - get() = fileName + get() = event?.uid constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { this.fileName = fileName @@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource { /* custom queries */ - override fun prepareForUpload() { + override fun prepareForUpload(fileName_: String?) { var uid: String? = null val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) if (c.moveToNext()) @@ -142,14 +142,14 @@ class LocalEvent : AndroidEvent, LocalResource { uid = UUID.randomUUID().toString() c.close() - val newFileName = uid + val fileName = fileName_ ?: uid val values = ContentValues(2) - values.put(Events._SYNC_ID, newFileName) + values.put(Events._SYNC_ID, fileName) values.put(COLUMN_UID, uid) calendar.provider.update(eventSyncURI(), values, null, null) - fileName = newFileName + this.fileName = fileName val event = this.event if (event != null) @@ -162,10 +162,12 @@ class LocalEvent : AndroidEvent, LocalResource { calendar.provider.update(eventSyncURI(), values, null, null) } - override fun clearDirty(eTag: String) { + override fun clearDirty(eTag: String?) { val values = ContentValues(2) values.put(CalendarContract.Events.DIRTY, 0) - values.put(COLUMN_ETAG, eTag) + if (eTag != null) { + values.put(COLUMN_ETAG, eTag) + } if (event != null) values.put(COLUMN_SEQUENCE, event?.sequence) calendar.provider.update(eventSyncURI(), values, null, null) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index e0ff5cf6..2f8192ed 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -120,13 +120,15 @@ class LocalGroup : AndroidGroup, LocalAddress { return values } - override fun clearDirty(eTag: String) { + override fun clearDirty(eTag: String?) { val id = requireNotNull(id) val values = ContentValues(2) values.put(Groups.DIRTY, 0) this.eTag = eTag - values.put(AndroidGroup.COLUMN_ETAG, eTag) + if (eTag != null) { + values.put(AndroidGroup.COLUMN_ETAG, eTag) + } update(values) // update cached group memberships @@ -154,15 +156,16 @@ class LocalGroup : AndroidGroup, LocalAddress { batch.commit() } - override fun prepareForUpload() { + override fun prepareForUpload(fileName_: String?) { val uid = UUID.randomUUID().toString() val values = ContentValues(2) - values.put(AndroidGroup.COLUMN_FILENAME, uid) + val fileName = fileName_ ?: uid + values.put(AndroidGroup.COLUMN_FILENAME, fileName) values.put(AndroidGroup.COLUMN_UID, uid) update(values) - fileName = uid + this.fileName = fileName } override fun resetDeleted() { diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt index e275fa49..92d77185 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt @@ -10,6 +10,7 @@ package com.etesync.syncadapter.resource interface LocalResource { val uuid: String? + val fileName: String? /** True if doesn't exist on server yet, false otherwise. */ val isLocalOnly: Boolean @@ -19,9 +20,10 @@ interface LocalResource { fun delete(): Int - fun prepareForUpload() + // FIXME: The null is for legacy + fun prepareForUpload(fileName: String?) - fun clearDirty(eTag: String) + fun clearDirty(eTag: String?) fun resetDeleted() } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index 9588a29d..1aa3b4c8 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -31,7 +31,7 @@ class LocalTask : AndroidTask, LocalResource { private var saveAsDirty = false // When true, the resource will be saved as dirty - private var fileName: String? = null + override var fileName: String? = null var eTag: String? = null override val content: String @@ -49,7 +49,7 @@ class LocalTask : AndroidTask, LocalResource { override// Now the same val uuid: String? - get() = fileName + get() = task?.uid constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?) : super(taskList, task) { @@ -96,7 +96,7 @@ class LocalTask : AndroidTask, LocalResource { /* custom queries */ - override fun prepareForUpload() { + override fun prepareForUpload(fileName_: String?) { var uid: String? = null val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null) if (c.moveToNext()) @@ -106,12 +106,13 @@ class LocalTask : AndroidTask, LocalResource { c.close() + val fileName = fileName_ ?: uid val values = ContentValues(2) - values.put(TaskContract.Tasks._SYNC_ID, uid) + values.put(TaskContract.Tasks._SYNC_ID, fileName) values.put(COLUMN_UID, uid) taskList.provider.client.update(taskSyncURI(), values, null, null) - fileName = uid + this.fileName = fileName val task = this.task if (task != null) task.uid = uid @@ -123,10 +124,12 @@ class LocalTask : AndroidTask, LocalResource { taskList.provider.client.update(taskSyncURI(), values, null, null) } - override fun clearDirty(eTag: String) { + override fun clearDirty(eTag: String?) { val values = ContentValues(2) values.put(TaskContract.Tasks._DIRTY, 0) - values.put(COLUMN_ETAG, eTag) + if (eTag != null) { + values.put(COLUMN_ETAG, eTag) + } if (task != null) values.put(COLUMN_SEQUENCE, task?.sequence) taskList.provider.client.update(taskSyncURI(), values, null, null) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index 6d0a760b..2ef6ed3f 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -134,8 +134,8 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } @Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class) - override fun createLocalEntries() { - super.createLocalEntries() + override fun prepareLocal() { + super.prepareLocal() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { createInviteAttendeesNotification() diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 4879da4e..ebb9b9bf 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -118,7 +118,7 @@ constructor(protected val context: Context, protected val account: Account, prot etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) colMgr = etebase.collectionManager synchronized(etebaseLocalCache) { - cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid) + cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid)!! } itemMgr = colMgr.getItemManager(cachedCollection.col) } @@ -199,6 +199,27 @@ constructor(protected val context: Context, protected val account: Account, prot etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid) } // Push local changes + var chunkPushItems: List + do { + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_prepare_local + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + prepareLocal() + + /* Create push items out of local changes. */ + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_create_local_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + chunkPushItems = createPushItems() + + if (Thread.interrupted()) + throw InterruptedException() + syncPhase = R.string.sync_phase_push_entries + Logger.log.info("Sync phase: " + context.getString(syncPhase)) + pushItems(chunkPushItems) + } while (chunkPushItems.size == MAX_PUSH) do { if (Thread.interrupted()) @@ -239,6 +260,11 @@ constructor(protected val context: Context, protected val account: Account, prot } catch (e: SSLHandshakeException) { syncResult.stats.numIoExceptions++ + notificationManager.setThrowable(e) + val detailsIntent = notificationManager.detailsIntent + detailsIntent.putExtra(KEY_ACCOUNT, account) + notificationManager.notify(syncErrorTitle, context.getString(syncPhase)) + } catch (e: FileNotFoundException) { notificationManager.setThrowable(e) val detailsIntent = notificationManager.detailsIntent detailsIntent.putExtra(KEY_ACCOUNT, account) @@ -395,7 +421,7 @@ constructor(protected val context: Context, protected val account: Account, prot Logger.log.info("Fetched items. Done=${ret.isDone}") return ret } else { - Logger.log.info("Skipping fetch because local lastUid == remoteLastUid (${remoteCTag})") + Logger.log.info("Skipping fetch because local stoken == lastStoken (${remoteCTag})") return null } } @@ -527,8 +553,127 @@ constructor(protected val context: Context, protected val account: Account, prot } } + private fun pushItems(chunkPushItems_: List) { + var chunkPushItems = chunkPushItems_ + // upload dirty contacts + var pushed = 0 + try { + if (!chunkPushItems.isEmpty()) { + val items = chunkPushItems + itemMgr.batch(items.toTypedArray()) + + // Persist the items + synchronized(etebaseLocalCache) { + val colUid = cachedCollection.col.uid + + for (item in items) { + etebaseLocalCache.itemSet(itemMgr, colUid, item) + } + } + + pushed += items.size + } + } finally { + // FIXME: A bit fragile, we assume the order in createPushItems + var left = pushed + for (local in localDeleted!!) { + if (pushed-- <= 0) { + break + } + local.delete() + } + if (left > 0) { + localDeleted = localDeleted?.drop(left) + chunkPushItems = chunkPushItems.drop(left - pushed) + } + + left = pushed + var i = 0 + for (local in localDirty) { + if (pushed-- <= 0) { + break + } + Logger.log.info("Added/changed resource with filename: " + local.fileName) + local.clearDirty(chunkPushItems[i].etag) + i++ + } + if (left > 0) { + localDirty = localDirty.drop(left) + chunkPushItems.drop(left) + } + + if (pushed > 0) { + Logger.log.severe("Unprocessed localentries left, this should never happen!") + } + } + } + + private fun itemUpdateMtime(item: Item) { + val meta = item.meta + meta.setMtime(System.currentTimeMillis()) + item.meta = meta + } + + private fun createPushItems(): List { + val ret = LinkedList() + val colUid = cachedCollection.col.uid + + synchronized(etebaseLocalCache) { + for (local in localDeleted!!) { + val item = etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!!.item + itemUpdateMtime(item) + item.delete() + ret.add(item) + + if (ret.size == MAX_PUSH) { + return ret + } + } + } + + synchronized(etebaseLocalCache) { + for (local in localDirty) { + val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!! else null + val item: Item + if (cacheItem != null) { + item = cacheItem.item + itemUpdateMtime(item) + } else { + val meta = ItemMetadata() + meta.name = local.uuid + meta.setMtime(System.currentTimeMillis()) + item = itemMgr.create(meta, "") + + local.prepareForUpload(item.uid) + } + + try { + item.setContent(local.content) + } catch (e: Exception) { + Logger.log.warning("Failed creating local entry ${local.uuid}") + if (local is LocalContact) { + Logger.log.warning("Contact with title ${local.contact?.displayName}") + } else if (local is LocalEvent) { + Logger.log.warning("Event with title ${local.event?.summary}") + } else if (local is LocalTask) { + Logger.log.warning("Task with title ${local.task?.summary}") + } + throw e + } + + ret.add(item) + + if (ret.size == MAX_PUSH) { + return ret + } + } + } + + return ret + } + @Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class) - protected open fun createLocalEntries() { + private fun createLocalEntries() { localEntries = LinkedList() // Not saving, just creating a fake one until we load it from a local db @@ -581,7 +726,7 @@ constructor(protected val context: Context, protected val account: Account, prot /** */ @Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class) - private fun prepareLocal() { + protected open fun prepareLocal() { localDeleted = processLocallyDeleted() localDirty = localCollection!!.findDirty(MAX_PUSH) // This is done after fetching the local dirty so all the ones we are using will be prepared @@ -598,7 +743,8 @@ constructor(protected val context: Context, protected val account: Account, prot val localList = localCollection!!.findDeleted() val ret = ArrayList(localList.size) - if (journalEntity.isReadOnly) { + val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro")) + if (readOnly) { for (local in localList) { Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}") local.resetDeleted() @@ -612,8 +758,11 @@ constructor(protected val context: Context, protected val account: Account, prot if (local.uuid != null) { Logger.log.info(local.uuid + " has been deleted locally -> deleting from server") } else { - Logger.log.fine("Entry deleted before ever syncing - genarting a UUID") - local.prepareForUpload() + if (isLegacy) { + // It's done later for non-legacy + Logger.log.fine("Entry deleted before ever syncing - genarting a UUID") + local.prepareForUpload(null) + } } ret.add(local) @@ -627,20 +776,22 @@ constructor(protected val context: Context, protected val account: Account, prot @Throws(CalendarStorageException::class, ContactsStorageException::class) protected open fun prepareDirty() { - if (journalEntity.isReadOnly) { + val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro")) + if (readOnly) { for (local in localDirty) { Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}") if (local.uuid == null) { // If it was only local, delete. local.delete() } else { - local.clearDirty(local.uuid!!) + local.clearDirty(null) } numDiscarded++ } localDirty = LinkedList() - } else { + } else if (isLegacy) { + // It's done later for non-legacy // assign file names and UIDs to new entries Logger.log.info("Looking for local entries without a uuid") for (local in localDirty) { @@ -649,7 +800,7 @@ constructor(protected val context: Context, protected val account: Account, prot } Logger.log.fine("Found local record without file name; generating file name/UID if necessary") - local.prepareForUpload() + local.prepareForUpload(null) } } } From 52b7a84a1a52cbdc7306cdee7eb48606fc1c88af Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 26 Aug 2020 18:48:15 +0300 Subject: [PATCH 26/83] Sync adapter: handle some etebase exceptions. --- .../syncadapter/syncadapter/SyncAdapterService.kt | 11 ++++++++++- .../etesync/syncadapter/syncadapter/SyncManager.kt | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt index ecc1bd6e..897b3171 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt @@ -23,6 +23,9 @@ import androidx.core.util.Pair import at.bitfire.ical4android.CalendarStorageException import at.bitfire.vcard4android.ContactsStorageException import com.etebase.client.FetchOptions +import com.etebase.client.exceptions.ConnectionException +import com.etebase.client.exceptions.TemporaryServerErrorException +import com.etebase.client.exceptions.UnauthorizedException import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions @@ -118,6 +121,12 @@ abstract class SyncAdapterService : Service() { } catch (e: Exceptions.ServiceUnavailableException) { syncResult.stats.numIoExceptions++ syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY + } catch (e: TemporaryServerErrorException) { + syncResult.stats.numIoExceptions++ + syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY + } catch (e: ConnectionException) { + syncResult.stats.numIoExceptions++ + syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY } catch (e: Exceptions.IgnorableHttpException) { // Ignore } catch (e: Exception) { @@ -133,7 +142,7 @@ abstract class SyncAdapterService : Service() { val detailsIntent = notificationManager.detailsIntent detailsIntent.putExtra(Constants.KEY_ACCOUNT, account) - if (e !is Exceptions.UnauthorizedException) { + if (e !is Exceptions.UnauthorizedException && e !is UnauthorizedException) { detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority) detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase) } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index ebb9b9bf..1c4a3bc1 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -17,6 +17,10 @@ import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.vcard4android.ContactsStorageException import com.etebase.client.* +import com.etebase.client.exceptions.ConnectionException +import com.etebase.client.exceptions.HttpException +import com.etebase.client.exceptions.TemporaryServerErrorException +import com.etebase.client.exceptions.UnauthorizedException import com.etesync.syncadapter.* import com.etesync.syncadapter.Constants.KEY_ACCOUNT import com.etesync.journalmanager.Crypto @@ -275,15 +279,21 @@ constructor(protected val context: Context, protected val account: Account, prot } catch (e: Exceptions.ServiceUnavailableException) { syncResult.stats.numIoExceptions++ syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY + } catch (e: TemporaryServerErrorException) { + syncResult.stats.numIoExceptions++ + syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY + } catch (e: ConnectionException) { + syncResult.stats.numIoExceptions++ + syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY } catch (e: InterruptedException) { // Restart sync if interrupted syncResult.fullSyncRequested = true } catch (e: Exceptions.IgnorableHttpException) { // Ignore } catch (e: Exception) { - if (e is Exceptions.UnauthorizedException) { + if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) { syncResult.stats.numAuthExceptions++ - } else if (e is Exceptions.HttpException) { + } else if (e is Exceptions.HttpException || e is HttpException) { syncResult.stats.numParseExceptions++ } else if (e is CalendarStorageException || e is ContactsStorageException) { syncResult.databaseError = true From 481dcc1944f9c71c851f9651dfb3d14a70d5612a Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 15:43:29 +0300 Subject: [PATCH 27/83] SyncManager: handle stoken being null (empty collection). --- .../java/com/etesync/syncadapter/syncadapter/SyncManager.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 1c4a3bc1..ab35b2e0 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -242,8 +242,10 @@ constructor(protected val context: Context, protected val account: Account, prot applyRemoteItems(itemList) stoken = itemList.stoken - synchronized(etebaseLocalCache) { - etebaseLocalCache.collectionSaveStoken(cachedCollection.col.uid, stoken!!) + if (stoken != null) { + synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionSaveStoken(cachedCollection.col.uid, stoken) + } } } while (!itemList!!.isDone) } From 63a8bf91a99cbb4ff39115b25f500991aad1151e Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 09:27:06 +0300 Subject: [PATCH 28/83] ViewCollection: add a basic etebase collection viewing activity. --- app/src/main/AndroidManifest.xml | 4 + .../etesync/syncadapter/EtebaseLocalCache.kt | 6 +- .../etesync/syncadapter/ui/AccountActivity.kt | 7 +- .../ui/etebase/CollectionActivity.kt | 103 +++++++++++++ .../ui/etebase/ListEntriesFragment.kt | 140 ++++++++++++++++++ .../ui/etebase/ViewCollectionFragment.kt | 137 +++++++++++++++++ .../layout/etebase_collection_activity.xml | 11 ++ .../res/layout/view_collection_fragment.xml | 68 +++++++++ .../res/menu/fragment_view_collection.xml | 27 ++++ 9 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt create mode 100644 app/src/main/res/layout/etebase_collection_activity.xml create mode 100644 app/src/main/res/layout/view_collection_fragment.xml create mode 100644 app/src/main/res/menu/fragment_view_collection.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45c699a0..d40e92c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -233,6 +233,10 @@ android:exported="false" android:parentActivityName=".ui.AccountsActivity"> + val info = adapter.getItem(position) as CollectionListItemInfo - startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!)) + if (settings.isLegacy) { + startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!)) + } else { + startActivity(CollectionActivity.newIntent(this@AccountActivity, account, info.uid)) + } } private val formattedFingerprint: String? diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt new file mode 100644 index 00000000..b762f6d1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -0,0 +1,103 @@ +package com.etesync.syncadapter.ui.etebase + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.observe +import com.etebase.client.CollectionManager +import com.etesync.syncadapter.* +import com.etesync.syncadapter.ui.BaseActivity +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread + +class CollectionActivity() : BaseActivity() { + private lateinit var account: Account + private lateinit var colUid: String + private val model: AccountCollectionViewModel by viewModels() + private val itemsModel: ItemsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!! + colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)!! + + setContentView(R.layout.etebase_collection_activity) + + if (savedInstanceState == null) { + model.loadCollection(this, account, colUid) + model.observe(this) { + itemsModel.loadItems(it) + } + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, ViewCollectionFragment()) + .commit() + } + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + companion object { + private val EXTRA_ACCOUNT = "account" + private val EXTRA_COLLECTION_UID = "collectionUid" + + fun newIntent(context: Context, account: Account, colUid: String): Intent { + val intent = Intent(context, CollectionActivity::class.java) + intent.putExtra(EXTRA_ACCOUNT, account) + intent.putExtra(EXTRA_COLLECTION_UID, colUid) + return intent + } + } +} + +class AccountCollectionViewModel : ViewModel() { + private val collection = MutableLiveData() + + fun loadCollection(context: Context, account: Account, colUid: String) { + doAsync { + val settings = AccountSettings(context, account) + val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) + val etebase = EtebaseLocalCache.getEtebase(context, HttpClient.sharedClient, settings) + val colMgr = etebase.collectionManager + val cachedCollection = synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionGet(colMgr, colUid)!! + } + uiThread { + collection.value = AccountCollectionHolder( + etebaseLocalCache, + etebase, + colMgr, + cachedCollection + ) + } + } + } + + fun observe(owner: LifecycleOwner, observer: (AccountCollectionHolder) -> Unit) = + collection.observe(owner, observer) +} + +data class AccountCollectionHolder(val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager, val cachedCollection: CachedCollection) + +class ItemsViewModel : ViewModel() { + private val cachedItems = MutableLiveData>() + + fun loadItems(accountCollectionHolder: AccountCollectionHolder) { + doAsync { + val col = accountCollectionHolder.cachedCollection.col + val itemMgr = accountCollectionHolder.colMgr.getItemManager(col) + val items = accountCollectionHolder.etebaseLocalCache.itemList(itemMgr, col.uid, withDeleted = true) + uiThread { + cachedItems.value = items + } + } + } + + fun observe(owner: LifecycleOwner, observer: (List) -> Unit) = + cachedItems.observe(owner, observer) +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt new file mode 100644 index 00000000..eed06aa4 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -0,0 +1,140 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.fragment.app.ListFragment +import androidx.fragment.app.activityViewModels +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.CachedItem +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import java.text.SimpleDateFormat +import java.util.concurrent.Future + +class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { + private val model: AccountCollectionViewModel by activityViewModels() + private val itemsModel: ItemsViewModel by activityViewModels() + private var asyncTask: Future? = null + + private var emptyTextView: TextView? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.journal_viewer_list, container, false) + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = view.findViewById(android.R.id.empty) as TextView + + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + model.observe(this) { col -> + itemsModel.observe(this) { + val entries = it.sortedByDescending { item -> + item.meta.mtime ?: 0 + } + val listAdapter = EntriesListAdapter(requireContext(), col.cachedCollection) + setListAdapter(listAdapter) + + listAdapter.addAll(entries) + + emptyTextView!!.text = getString(R.string.journal_entries_list_empty) + } + } + + listView.onItemClickListener = this + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val item = listAdapter?.getItem(position) as CachedItem + Toast.makeText(context, "Clicked ${item.item.uid}", Toast.LENGTH_LONG).show() + // startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content)) + } + + internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { + + override fun getView(position: Int, _v: View?, parent: ViewGroup): View { + var v = _v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!! + + val item = getItem(position) + + setItemView(v, cachedCollection.meta.collectionType, item) + + /* FIXME: handle entry error: + val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull() + if (entryError != null) { + val errorIcon = v.findViewById(R.id.error) as ImageView + errorIcon.visibility = View.VISIBLE + } + */ + + return v + } + } + + companion object { + private val dateFormatter = SimpleDateFormat() + private fun getLine(content: String?, prefix: String): String? { + var content: String? = content ?: return null + + val start = content!!.indexOf(prefix) + if (start >= 0) { + val end = content.indexOf("\n", start) + content = content.substring(start + prefix.length, end) + } else { + content = null + } + return content + } + + fun setItemView(v: View, collectionType: String, item: CachedItem) { + + var tv = v.findViewById(R.id.title) as TextView + + // FIXME: hacky way to make it show sensible info + val prefix: String = when (collectionType) { + Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> { + "SUMMARY:" + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + "FN:" + } + else -> { + "" + } + } + + val fullContent = item.content + var content = getLine(fullContent, prefix) + content = content ?: "Not found" + tv.text = content + + tv = v.findViewById(R.id.description) as TextView + // FIXME: Don't use a hard-coded string + content = "Modified: ${dateFormatter.format(item.meta.mtime ?: 0)}" + tv.text = content + + val action = v.findViewById(R.id.action) as ImageView + if (item.item.isDeleted) { + action.setImageResource(R.drawable.action_delete) + } else { + action.setImageResource(R.drawable.action_change) + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt new file mode 100644 index 00000000..6e89f503 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -0,0 +1,137 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color.parseColor +import android.os.Bundle +import android.view.* +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.ui.BaseActivity +import com.etesync.syncadapter.ui.WebViewActivity +import com.etesync.syncadapter.utils.HintManager +import com.etesync.syncadapter.utils.ShowcaseBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton +import tourguide.tourguide.ToolTip +import java.util.* + +class ViewCollectionFragment : Fragment() { + private val model: AccountCollectionViewModel by activityViewModels() + private val itemsModel: ItemsViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = super.onCreateView(inflater, container, savedInstanceState) + + inflater.inflate(R.layout.view_collection_fragment, container) + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + model.observe(this) { + if (container != null) { + initUi(inflater, container, it) + } + } + } + + return ret + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + model.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.title = it.cachedCollection.meta.name + } + } + + private fun initUi(inflater: LayoutInflater, container: ViewGroup, collectionHolder: AccountCollectionHolder) { + val title = container.findViewById(R.id.display_name) + if (!HintManager.getHintSeen(requireContext(), HINT_IMPORT)) { + val tourGuide = ShowcaseBuilder.getBuilder(requireActivity()) + .setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_import)).setGravity(Gravity.BOTTOM)) + .setPointer(null) + tourGuide.mOverlay.setHoleRadius(0) + tourGuide.playOn(title) + HintManager.setHintSeen(requireContext(), HINT_IMPORT, true) + } + + val fab = container.findViewById(R.id.fab) + fab?.setOnClickListener { + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.use_native_apps_title) + .setMessage(R.string.use_native_apps_body) + .setNegativeButton(R.string.navigation_drawer_guide, { _: DialogInterface, _: Int -> WebViewActivity.openUrl(requireContext(), Constants.helpUri) }) + .setPositiveButton(android.R.string.yes) { _, _ -> }.show() + } + + val col = collectionHolder.cachedCollection.col + val meta = collectionHolder.cachedCollection.meta + val isAdmin = col.accessLevel == "adm" + + val colorSquare = container.findViewById(R.id.color) + val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + when (meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR -> { + colorSquare.setBackgroundColor(color) + } + Constants.ETEBASE_TYPE_TASKS -> { + colorSquare.setBackgroundColor(color) + val tasksNotShowing = container.findViewById(R.id.tasks_not_showing) + tasksNotShowing.visibility = View.VISIBLE + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + colorSquare.visibility = View.GONE + } + } + + title.text = meta.name + + val desc = container.findViewById(R.id.description) + desc.text = meta.description + + val owner = container.findViewById(R.id.owner) + if (isAdmin) { + owner.visibility = View.GONE + } else { + owner.visibility = View.VISIBLE + owner.text = "Shared with us" // FIXME: Figure out how to represent it and don't use a hardcoded string + } + + itemsModel.observe(this) { + val stats = container.findViewById(R.id.stats) + container.findViewById(R.id.progressBar).visibility = View.GONE + stats.text = String.format(Locale.getDefault(), "Change log items: %d", it.size) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.fragment_view_collection, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.on_edit -> { + Toast.makeText(context, "Edit", Toast.LENGTH_LONG).show() + } + R.id.on_manage_members -> { + Toast.makeText(context, "Manage", Toast.LENGTH_LONG).show() + } + R.id.on_import -> { + Toast.makeText(context, "Import", Toast.LENGTH_LONG).show() + } + } + return super.onOptionsItemSelected(item) + } + + companion object { + private val HINT_IMPORT = "Import" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/etebase_collection_activity.xml b/app/src/main/res/layout/etebase_collection_activity.xml new file mode 100644 index 00000000..b500265d --- /dev/null +++ b/app/src/main/res/layout/etebase_collection_activity.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/view_collection_fragment.xml b/app/src/main/res/layout/view_collection_fragment.xml new file mode 100644 index 00000000..1b940572 --- /dev/null +++ b/app/src/main/res/layout/view_collection_fragment.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/fragment_view_collection.xml b/app/src/main/res/menu/fragment_view_collection.xml new file mode 100644 index 00000000..42ff6209 --- /dev/null +++ b/app/src/main/res/menu/fragment_view_collection.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file From be22beb7f988cc67f35c0ac6cbf3c1a00c648784 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 13:55:57 +0300 Subject: [PATCH 29/83] EditCollection: add a fragment to edit collection. --- .../ui/etebase/CollectionActivity.kt | 28 ++- .../ui/etebase/EditCollectionFragment.kt | 235 ++++++++++++++++++ .../ui/etebase/ViewCollectionFragment.kt | 23 +- .../res/menu/fragment_edit_collection.xml | 25 ++ 4 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt create mode 100644 app/src/main/res/menu/fragment_edit_collection.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt index b762f6d1..809d2fa0 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.fragment.app.commit import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -34,9 +35,9 @@ class CollectionActivity() : BaseActivity() { model.observe(this) { itemsModel.loadItems(it) } - supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, ViewCollectionFragment()) - .commit() + supportFragmentManager.commit { + replace(R.id.fragment_container, ViewCollectionFragment()) + } } supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -69,6 +70,7 @@ class AccountCollectionViewModel : ViewModel() { } uiThread { collection.value = AccountCollectionHolder( + account, etebaseLocalCache, etebase, colMgr, @@ -80,9 +82,12 @@ class AccountCollectionViewModel : ViewModel() { fun observe(owner: LifecycleOwner, observer: (AccountCollectionHolder) -> Unit) = collection.observe(owner, observer) + + val value: AccountCollectionHolder? + get() = collection.value } -data class AccountCollectionHolder(val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager, val cachedCollection: CachedCollection) +data class AccountCollectionHolder(val account: Account, val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager, val cachedCollection: CachedCollection) class ItemsViewModel : ViewModel() { private val cachedItems = MutableLiveData>() @@ -100,4 +105,19 @@ class ItemsViewModel : ViewModel() { fun observe(owner: LifecycleOwner, observer: (List) -> Unit) = cachedItems.observe(owner, observer) + + val value: List? + get() = cachedItems.value +} + + +class LoadingViewModel : ViewModel() { + private val loading = MutableLiveData() + + fun setLoading(value: Boolean) { + loading.value = value + } + + fun observe(owner: LifecycleOwner, observer: (Boolean) -> Unit) = + loading.observe(owner, observer) } \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt new file mode 100644 index 00000000..0b08a8a7 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt @@ -0,0 +1,235 @@ +package com.etesync.syncadapter.ui.etebase + +import android.graphics.Color.parseColor +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.* +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import com.etebase.client.Collection +import com.etebase.client.exceptions.EtebaseException +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.ui.BaseActivity +import org.apache.commons.lang3.StringUtils +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import yuku.ambilwarna.AmbilWarnaDialog +import java.lang.String + +class EditCollectionFragment() : Fragment() { + private val model: AccountCollectionViewModel by activityViewModels() + private val loadingModel: LoadingViewModel by viewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = inflater.inflate(R.layout.activity_create_collection, container, false) + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + model.observe(this) { + updateTitle(it) + if (container != null) { + initUi(inflater, ret, it) + } + } + } + + return ret + } + + fun updateTitle(accountCollectionHolder: AccountCollectionHolder) { + accountCollectionHolder.let { + val new = false + var titleId: Int = R.string.create_calendar + if (new) { + when (it.cachedCollection.meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR -> { + titleId = R.string.create_calendar + } + Constants.ETEBASE_TYPE_TASKS -> { + titleId = R.string.create_tasklist + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + titleId = R.string.create_addressbook + } + } + } else { + titleId = R.string.edit_collection + } + (activity as? BaseActivity?)?.supportActionBar?.setTitle(titleId) + } + } + + private fun initUi(inflater: LayoutInflater, v: View, collectionHolder: AccountCollectionHolder) { + val title = v.findViewById(R.id.display_name) + val desc = v.findViewById(R.id.description) + + val meta = collectionHolder.cachedCollection.meta + + title.setText(meta.name) + desc.setText(meta.description) + + val colorSquare = v.findViewById(R.id.color) + when (collectionHolder.cachedCollection.meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR -> { + title.setHint(R.string.create_calendar_display_name_hint) + + val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + colorSquare.setBackgroundColor(color) + colorSquare.setOnClickListener { + AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { + override fun onCancel(dialog: AmbilWarnaDialog) {} + + override fun onOk(dialog: AmbilWarnaDialog, color: Int) { + colorSquare.setBackgroundColor(color) + } + }).show() + } + } + Constants.ETEBASE_TYPE_TASKS -> { + title.setHint(R.string.create_tasklist_display_name_hint) + + val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + colorSquare.setBackgroundColor(color) + colorSquare.setOnClickListener { + AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { + override fun onCancel(dialog: AmbilWarnaDialog) {} + + override fun onOk(dialog: AmbilWarnaDialog, color: Int) { + colorSquare.setBackgroundColor(color) + } + }).show() + } + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + title.setHint(R.string.create_addressbook_display_name_hint) + + val colorGroup = v.findViewById(R.id.color_group) + colorGroup.visibility = View.GONE + } + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.fragment_edit_collection, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.on_delete -> { + deleteColection() + } + R.id.on_save -> { + saveCollection() + } + } + return super.onOptionsItemSelected(item) + } + + private fun deleteColection() { + val meta = model.value!!.cachedCollection.meta + val name = meta.name + + AlertDialog.Builder(requireContext()) + .setTitle(R.string.delete_collection_confirm_title) + .setMessage(getString(R.string.delete_collection_confirm_warning, name)) + .setPositiveButton(android.R.string.yes) { dialog, _ -> + doDeleteCollection() + dialog.dismiss() + } + .setNegativeButton(android.R.string.no) { _, _ -> } + .show() + } + + private fun doDeleteCollection() { + loadingModel.setLoading(true) + doAsync { + try { + val col = model.value!!.cachedCollection.col + col.delete() + uploadCollection(col) + activity?.finish() + } catch (e: EtebaseException) { + uiThread { + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.exception) + .setMessage(e.localizedMessage) + .setPositiveButton(android.R.string.yes) { _, _ -> }.show() + } + } finally { + uiThread { + loadingModel.setLoading(false) + } + } + } + } + + private fun saveCollection() { + var ok = true + + val meta = model.value!!.cachedCollection.meta + val v = requireView() + + var edit = v.findViewById(R.id.display_name) + meta.name = edit.text.toString() + if (TextUtils.isEmpty(meta.name)) { + edit.error = getString(R.string.create_collection_display_name_required) + ok = false + } + + edit = v.findViewById(R.id.description) + meta.description = StringUtils.trimToNull(edit.text.toString()) + + if (ok) { + when (meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> { + val view = v.findViewById(R.id.color) + val color = (view.background as ColorDrawable).color + meta.color = String.format("#%06X", 0xFFFFFF and color) + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + } + } + + loadingModel.setLoading(true) + doAsync { + try { + val col = model.value!!.cachedCollection.col + col.meta = meta + uploadCollection(col) + parentFragmentManager.popBackStack() + } catch (e: EtebaseException) { + uiThread { + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.exception) + .setMessage(e.localizedMessage) + .setPositiveButton(android.R.string.yes) { _, _ -> }.show() + } + } finally { + uiThread { + loadingModel.setLoading(false) + } + } + } + } + } + + private fun uploadCollection(col: Collection) { + val accountHolder = model.value!! + val etebaseLocalCache = accountHolder.etebaseLocalCache + val colMgr = accountHolder.colMgr + colMgr.upload(col) + synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionSet(colMgr, col) + } + model.loadCollection(requireContext(), accountHolder.account, col.uid) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index 6e89f503..6a1c52fd 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar @@ -26,15 +27,14 @@ class ViewCollectionFragment : Fragment() { private val itemsModel: ItemsViewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val ret = super.onCreateView(inflater, container, savedInstanceState) - - inflater.inflate(R.layout.view_collection_fragment, container) + val ret = inflater.inflate(R.layout.view_collection_fragment, container, false) setHasOptionsMenu(true) if (savedInstanceState == null) { model.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.title = it.cachedCollection.meta.name if (container != null) { - initUi(inflater, container, it) + initUi(inflater, ret, it) } } } @@ -42,15 +42,7 @@ class ViewCollectionFragment : Fragment() { return ret } - override fun onAttach(context: Context) { - super.onAttach(context) - - model.observe(this) { - (activity as? BaseActivity?)?.supportActionBar?.title = it.cachedCollection.meta.name - } - } - - private fun initUi(inflater: LayoutInflater, container: ViewGroup, collectionHolder: AccountCollectionHolder) { + private fun initUi(inflater: LayoutInflater, container: View, collectionHolder: AccountCollectionHolder) { val title = container.findViewById(R.id.display_name) if (!HintManager.getHintSeen(requireContext(), HINT_IMPORT)) { val tourGuide = ShowcaseBuilder.getBuilder(requireActivity()) @@ -119,7 +111,10 @@ class ViewCollectionFragment : Fragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.on_edit -> { - Toast.makeText(context, "Edit", Toast.LENGTH_LONG).show() + parentFragmentManager.commit { + replace(R.id.fragment_container, EditCollectionFragment()) + addToBackStack(EditCollectionFragment::class.java.name) + } } R.id.on_manage_members -> { Toast.makeText(context, "Manage", Toast.LENGTH_LONG).show() diff --git a/app/src/main/res/menu/fragment_edit_collection.xml b/app/src/main/res/menu/fragment_edit_collection.xml new file mode 100644 index 00000000..d9b5ec5d --- /dev/null +++ b/app/src/main/res/menu/fragment_edit_collection.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file From 74b4ef3ee9e2cb81ac0071b04b0c9ce6db79cf49 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 16:12:33 +0300 Subject: [PATCH 30/83] Overhaul Etebase collection activity. --- .../ui/etebase/CollectionActivity.kt | 85 +++++++++++++------ .../ui/etebase/EditCollectionFragment.kt | 54 ++++-------- .../ui/etebase/ListEntriesFragment.kt | 6 +- .../ui/etebase/ViewCollectionFragment.kt | 16 ++-- 4 files changed, 89 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt index 809d2fa0..32c28647 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -18,25 +18,35 @@ import org.jetbrains.anko.uiThread class CollectionActivity() : BaseActivity() { private lateinit var account: Account - private lateinit var colUid: String - private val model: AccountCollectionViewModel by viewModels() + private val model: AccountViewModel by viewModels() + private val collectionModel: CollectionViewModel by viewModels() private val itemsModel: ItemsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!! - colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)!! + val colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID) + val colType = intent.extras!!.getString(EXTRA_COLLECTION_TYPE) setContentView(R.layout.etebase_collection_activity) if (savedInstanceState == null) { - model.loadCollection(this, account, colUid) - model.observe(this) { - itemsModel.loadItems(it) - } - supportFragmentManager.commit { - replace(R.id.fragment_container, ViewCollectionFragment()) + model.loadAccount(this, account) + if (colUid != null) { + model.observe(this) { + collectionModel.loadCollection(it, colUid) + collectionModel.observe(this) { cachedCollection -> + itemsModel.loadItems(it, cachedCollection) + } + } + supportFragmentManager.commit { + replace(R.id.fragment_container, ViewCollectionFragment()) + } + } else if (colType != null) { + supportFragmentManager.commit { + // replace(R.id.fragment_container, CreateCollectionFragment(colType)) + } } } @@ -46,6 +56,7 @@ class CollectionActivity() : BaseActivity() { companion object { private val EXTRA_ACCOUNT = "account" private val EXTRA_COLLECTION_UID = "collectionUid" + private val EXTRA_COLLECTION_TYPE = "collectionType" fun newIntent(context: Context, account: Account, colUid: String): Intent { val intent = Intent(context, CollectionActivity::class.java) @@ -53,48 +64,74 @@ class CollectionActivity() : BaseActivity() { intent.putExtra(EXTRA_COLLECTION_UID, colUid) return intent } + + fun newCreateCollectionIntent(context: Context, account: Account, colType: String): Intent { + val intent = Intent(context, CollectionActivity::class.java) + intent.putExtra(EXTRA_ACCOUNT, account) + intent.putExtra(EXTRA_COLLECTION_TYPE, colType) + return intent + } } } -class AccountCollectionViewModel : ViewModel() { - private val collection = MutableLiveData() +class AccountViewModel : ViewModel() { + private val holder = MutableLiveData() - fun loadCollection(context: Context, account: Account, colUid: String) { + fun loadAccount(context: Context, account: Account) { doAsync { val settings = AccountSettings(context, account) val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) val etebase = EtebaseLocalCache.getEtebase(context, HttpClient.sharedClient, settings) val colMgr = etebase.collectionManager - val cachedCollection = synchronized(etebaseLocalCache) { - etebaseLocalCache.collectionGet(colMgr, colUid)!! - } uiThread { - collection.value = AccountCollectionHolder( + holder.value = AccountHolder( account, etebaseLocalCache, etebase, - colMgr, - cachedCollection + colMgr ) } } } - fun observe(owner: LifecycleOwner, observer: (AccountCollectionHolder) -> Unit) = + fun observe(owner: LifecycleOwner, observer: (AccountHolder) -> Unit) = + holder.observe(owner, observer) + + val value: AccountHolder? + get() = holder.value +} + +data class AccountHolder(val account: Account, val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager) + +class CollectionViewModel : ViewModel() { + private val collection = MutableLiveData() + + fun loadCollection(accountHolder: AccountHolder, colUid: String) { + doAsync { + val etebaseLocalCache = accountHolder.etebaseLocalCache + val colMgr = accountHolder.colMgr + val cachedCollection = synchronized(etebaseLocalCache) { + etebaseLocalCache.collectionGet(colMgr, colUid)!! + } + uiThread { + collection.value = cachedCollection + } + } + } + + fun observe(owner: LifecycleOwner, observer: (CachedCollection) -> Unit) = collection.observe(owner, observer) - val value: AccountCollectionHolder? + val value: CachedCollection? get() = collection.value } -data class AccountCollectionHolder(val account: Account, val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager, val cachedCollection: CachedCollection) - class ItemsViewModel : ViewModel() { private val cachedItems = MutableLiveData>() - fun loadItems(accountCollectionHolder: AccountCollectionHolder) { + fun loadItems(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) { doAsync { - val col = accountCollectionHolder.cachedCollection.col + val col = cachedCollection.col val itemMgr = accountCollectionHolder.colMgr.getItemManager(col) val items = accountCollectionHolder.etebaseLocalCache.itemList(itemMgr, col.uid, withDeleted = true) uiThread { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt index 0b08a8a7..62c88a41 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt @@ -12,6 +12,7 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import com.etebase.client.Collection import com.etebase.client.exceptions.EtebaseException +import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar @@ -20,10 +21,10 @@ import org.apache.commons.lang3.StringUtils import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread import yuku.ambilwarna.AmbilWarnaDialog -import java.lang.String -class EditCollectionFragment() : Fragment() { - private val model: AccountCollectionViewModel by activityViewModels() +class EditCollectionFragment(private val cachedCollection: CachedCollection) : Fragment() { + private val model: AccountViewModel by activityViewModels() + private val collectionModel: CollectionViewModel by activityViewModels() private val loadingModel: LoadingViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -31,51 +32,30 @@ class EditCollectionFragment() : Fragment() { setHasOptionsMenu(true) if (savedInstanceState == null) { - model.observe(this) { - updateTitle(it) - if (container != null) { - initUi(inflater, ret, it) - } + updateTitle() + if (container != null) { + initUi(inflater, ret) } } return ret } - fun updateTitle(accountCollectionHolder: AccountCollectionHolder) { - accountCollectionHolder.let { - val new = false - var titleId: Int = R.string.create_calendar - if (new) { - when (it.cachedCollection.meta.collectionType) { - Constants.ETEBASE_TYPE_CALENDAR -> { - titleId = R.string.create_calendar - } - Constants.ETEBASE_TYPE_TASKS -> { - titleId = R.string.create_tasklist - } - Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { - titleId = R.string.create_addressbook - } - } - } else { - titleId = R.string.edit_collection - } - (activity as? BaseActivity?)?.supportActionBar?.setTitle(titleId) - } + fun updateTitle() { + (activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.edit_collection) } - private fun initUi(inflater: LayoutInflater, v: View, collectionHolder: AccountCollectionHolder) { + private fun initUi(inflater: LayoutInflater, v: View) { val title = v.findViewById(R.id.display_name) val desc = v.findViewById(R.id.description) - val meta = collectionHolder.cachedCollection.meta + val meta = cachedCollection.meta title.setText(meta.name) desc.setText(meta.description) val colorSquare = v.findViewById(R.id.color) - when (collectionHolder.cachedCollection.meta.collectionType) { + when (cachedCollection.meta.collectionType) { Constants.ETEBASE_TYPE_CALENDAR -> { title.setHint(R.string.create_calendar_display_name_hint) @@ -133,7 +113,7 @@ class EditCollectionFragment() : Fragment() { } private fun deleteColection() { - val meta = model.value!!.cachedCollection.meta + val meta = cachedCollection.meta val name = meta.name AlertDialog.Builder(requireContext()) @@ -151,7 +131,7 @@ class EditCollectionFragment() : Fragment() { loadingModel.setLoading(true) doAsync { try { - val col = model.value!!.cachedCollection.col + val col = cachedCollection.col col.delete() uploadCollection(col) activity?.finish() @@ -174,7 +154,7 @@ class EditCollectionFragment() : Fragment() { private fun saveCollection() { var ok = true - val meta = model.value!!.cachedCollection.meta + val meta = cachedCollection.meta val v = requireView() var edit = v.findViewById(R.id.display_name) @@ -201,7 +181,7 @@ class EditCollectionFragment() : Fragment() { loadingModel.setLoading(true) doAsync { try { - val col = model.value!!.cachedCollection.col + val col = cachedCollection.col col.meta = meta uploadCollection(col) parentFragmentManager.popBackStack() @@ -230,6 +210,6 @@ class EditCollectionFragment() : Fragment() { synchronized(etebaseLocalCache) { etebaseLocalCache.collectionSet(colMgr, col) } - model.loadCollection(requireContext(), accountHolder.account, col.uid) + collectionModel.loadCollection(model.value!!, col.uid) } } \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt index eed06aa4..123ccabc 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -16,7 +16,7 @@ import java.text.SimpleDateFormat import java.util.concurrent.Future class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { - private val model: AccountCollectionViewModel by activityViewModels() + private val collectionModel: CollectionViewModel by activityViewModels() private val itemsModel: ItemsViewModel by activityViewModels() private var asyncTask: Future? = null @@ -35,12 +35,12 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - model.observe(this) { col -> + collectionModel.observe(this) { col -> itemsModel.observe(this) { val entries = it.sortedByDescending { item -> item.meta.mtime ?: 0 } - val listAdapter = EntriesListAdapter(requireContext(), col.cachedCollection) + val listAdapter = EntriesListAdapter(requireContext(), col) setListAdapter(listAdapter) listAdapter.addAll(entries) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index 6a1c52fd..a6f49253 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -1,6 +1,5 @@ package com.etesync.syncadapter.ui.etebase -import android.content.Context import android.content.DialogInterface import android.graphics.Color.parseColor import android.os.Bundle @@ -11,6 +10,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.commit +import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar @@ -23,7 +23,7 @@ import tourguide.tourguide.ToolTip import java.util.* class ViewCollectionFragment : Fragment() { - private val model: AccountCollectionViewModel by activityViewModels() + private val collectionModel: CollectionViewModel by activityViewModels() private val itemsModel: ItemsViewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -31,8 +31,8 @@ class ViewCollectionFragment : Fragment() { setHasOptionsMenu(true) if (savedInstanceState == null) { - model.observe(this) { - (activity as? BaseActivity?)?.supportActionBar?.title = it.cachedCollection.meta.name + collectionModel.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name if (container != null) { initUi(inflater, ret, it) } @@ -42,7 +42,7 @@ class ViewCollectionFragment : Fragment() { return ret } - private fun initUi(inflater: LayoutInflater, container: View, collectionHolder: AccountCollectionHolder) { + private fun initUi(inflater: LayoutInflater, container: View, cachedCollection: CachedCollection) { val title = container.findViewById(R.id.display_name) if (!HintManager.getHintSeen(requireContext(), HINT_IMPORT)) { val tourGuide = ShowcaseBuilder.getBuilder(requireActivity()) @@ -63,8 +63,8 @@ class ViewCollectionFragment : Fragment() { .setPositiveButton(android.R.string.yes) { _, _ -> }.show() } - val col = collectionHolder.cachedCollection.col - val meta = collectionHolder.cachedCollection.meta + val col = cachedCollection.col + val meta = cachedCollection.meta val isAdmin = col.accessLevel == "adm" val colorSquare = container.findViewById(R.id.color) @@ -112,7 +112,7 @@ class ViewCollectionFragment : Fragment() { when (item.itemId) { R.id.on_edit -> { parentFragmentManager.commit { - replace(R.id.fragment_container, EditCollectionFragment()) + replace(R.id.fragment_container, EditCollectionFragment(collectionModel.value!!)) addToBackStack(EditCollectionFragment::class.java.name) } } From b9d3dc691b93df2950c1bb5fb1753a8e1373d91d Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 16:39:11 +0300 Subject: [PATCH 31/83] Implement collection creation. --- .../etesync/syncadapter/ui/AccountActivity.kt | 30 +++++++++++----- .../ui/etebase/CollectionActivity.kt | 13 +++++-- .../ui/etebase/EditCollectionFragment.kt | 34 +++++++++++++++++-- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index fefa12d5..50f365a7 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -207,19 +207,31 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe val info: CollectionInfo when (item.itemId) { R.id.create_calendar -> { - info = CollectionInfo() - info.enumType = CollectionInfo.Type.CALENDAR - startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + if (settings.isLegacy) { + info = CollectionInfo() + info.enumType = CollectionInfo.Type.CALENDAR + startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + } else { + startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_CALENDAR)) + } } R.id.create_tasklist -> { - info = CollectionInfo() - info.enumType = CollectionInfo.Type.TASKS - startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + if (settings.isLegacy) { + info = CollectionInfo() + info.enumType = CollectionInfo.Type.TASKS + startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + } else { + startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_TASKS)) + } } R.id.create_addressbook -> { - info = CollectionInfo() - info.enumType = CollectionInfo.Type.ADDRESS_BOOK - startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + if (settings.isLegacy) { + info = CollectionInfo() + info.enumType = CollectionInfo.Type.ADDRESS_BOOK + startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info)) + } else { + startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_ADDRESS_BOOK)) + } } R.id.install_tasksorg -> { installPackage(tasksOrgPackage) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt index 32c28647..04da9dcd 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.observe import com.etebase.client.CollectionManager +import com.etebase.client.CollectionMetadata import com.etesync.syncadapter.* import com.etesync.syncadapter.ui.BaseActivity import org.jetbrains.anko.doAsync @@ -44,8 +45,16 @@ class CollectionActivity() : BaseActivity() { replace(R.id.fragment_container, ViewCollectionFragment()) } } else if (colType != null) { - supportFragmentManager.commit { - // replace(R.id.fragment_container, CreateCollectionFragment(colType)) + model.observe(this) { + doAsync { + val meta = CollectionMetadata(colType, "") + val cachedCollection = CachedCollection(it.colMgr.create(meta, ""), meta) + uiThread { + supportFragmentManager.commit { + replace(R.id.fragment_container, EditCollectionFragment(cachedCollection, true)) + } + } + } } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt index 62c88a41..2647c5d1 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt @@ -9,6 +9,7 @@ import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit import androidx.fragment.app.viewModels import com.etebase.client.Collection import com.etebase.client.exceptions.EtebaseException @@ -22,7 +23,7 @@ import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread import yuku.ambilwarna.AmbilWarnaDialog -class EditCollectionFragment(private val cachedCollection: CachedCollection) : Fragment() { +class EditCollectionFragment(private val cachedCollection: CachedCollection, private val isCreating: Boolean = false) : Fragment() { private val model: AccountViewModel by activityViewModels() private val collectionModel: CollectionViewModel by activityViewModels() private val loadingModel: LoadingViewModel by viewModels() @@ -42,7 +43,25 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection) : F } fun updateTitle() { - (activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.edit_collection) + cachedCollection.let { + var titleId: Int = R.string.create_calendar + if (isCreating) { + when (cachedCollection.meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR -> { + titleId = R.string.create_calendar + } + Constants.ETEBASE_TYPE_TASKS -> { + titleId = R.string.create_tasklist + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + titleId = R.string.create_addressbook + } + } + } else { + titleId = R.string.edit_collection + } + (activity as? BaseActivity?)?.supportActionBar?.setTitle(titleId) + } } private fun initUi(inflater: LayoutInflater, v: View) { @@ -98,6 +117,9 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection) : F override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.fragment_edit_collection, menu) + if (isCreating) { + menu.findItem(R.id.on_delete).setVisible(false) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -184,7 +206,13 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection) : F val col = cachedCollection.col col.meta = meta uploadCollection(col) - parentFragmentManager.popBackStack() + if (isCreating) { + parentFragmentManager.commit { + replace(R.id.fragment_container, ViewCollectionFragment()) + } + } else { + parentFragmentManager.popBackStack() + } } catch (e: EtebaseException) { uiThread { AlertDialog.Builder(requireContext()) From 290aa159b229de8205c1a293a8307020af7dc30d Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 16:44:55 +0300 Subject: [PATCH 32/83] Implement showing fingerprint. --- .../com/etesync/syncadapter/ui/AccountActivity.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 50f365a7..6d3445b1 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import at.bitfire.vcard4android.ContactsStorageException import com.etebase.client.CollectionManager +import com.etebase.client.Utils import com.etebase.client.exceptions.EtebaseException import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto @@ -82,13 +83,18 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe private val formattedFingerprint: String? get() { try { - val settings = AccountSettings(this, account) - return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey) + if (settings.isLegacy) { + val settings = AccountSettings(this, account) + return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey) + } else { + val etebase = EtebaseLocalCache.getEtebase(this, HttpClient.sharedClient, settings) + val invitationManager = etebase.invitationManager + return Utils.prettyFingerprint(invitationManager.pubkey) + } } catch (e: Exception) { e.printStackTrace() - return null + return e.localizedMessage } - } override fun onCreate(savedInstanceState: Bundle?) { From 71e37fb9a694230a007b04911cf26c83877db8be Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 16:55:43 +0300 Subject: [PATCH 33/83] Only allow collection owners to edit it. --- .../ui/etebase/ViewCollectionFragment.kt | 18 +++++++++++++++--- app/src/main/res/values/strings.xml | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index a6f49253..eb27eb59 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -15,6 +15,7 @@ import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar import com.etesync.syncadapter.ui.BaseActivity +import com.etesync.syncadapter.ui.EditCollectionActivity import com.etesync.syncadapter.ui.WebViewActivity import com.etesync.syncadapter.utils.HintManager import com.etesync.syncadapter.utils.ShowcaseBuilder @@ -109,11 +110,22 @@ class ViewCollectionFragment : Fragment() { } override fun onOptionsItemSelected(item: MenuItem): Boolean { + val cachedCollection = collectionModel.value!! + when (item.itemId) { R.id.on_edit -> { - parentFragmentManager.commit { - replace(R.id.fragment_container, EditCollectionFragment(collectionModel.value!!)) - addToBackStack(EditCollectionFragment::class.java.name) + if (cachedCollection.col.accessLevel == "adm") { + parentFragmentManager.commit { + replace(R.id.fragment_container, EditCollectionFragment(cachedCollection)) + addToBackStack(EditCollectionFragment::class.java.name) + } + } else { + val dialog = AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.edit_owner_only_anon) + .setPositiveButton(android.R.string.yes) { _, _ -> }.create() + dialog.show() } } R.id.on_manage_members -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f68c15d..f43d977e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -147,6 +147,7 @@ Only the owner of this collection (%s) is allowed to view its members. Not Allowed Only the owner of this collection (%s) is allowed to edit it. + Only the owner of this collection is allowed to edit it. Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support. Did you know? EteSync seamlessly integrates with Android, so to use it, just use your existing address book and calendar apps!\n\nFor more information, please check out the user guide. From cea7f8fdc668bfbf39c35acba29fc4a0ffefc1fb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 17:20:51 +0300 Subject: [PATCH 34/83] Implement showing changelog item. --- .../ui/etebase/CollectionItemFragment.kt | 436 ++++++++++++++++++ .../ui/etebase/ListEntriesFragment.kt | 7 +- .../res/menu/collection_item_fragment.xml | 26 ++ 3 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt create mode 100644 app/src/main/res/menu/collection_item_fragment.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt new file mode 100644 index 00000000..a42f4f14 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt @@ -0,0 +1,436 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.os.Bundle +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.view.* +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.activityViewModels +import androidx.viewpager.widget.ViewPager +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.ical4android.Task +import at.bitfire.vcard4android.Contact +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.CachedItem +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.BaseActivity +import com.google.android.material.tabs.TabLayout +import ezvcard.util.PartialDate +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.io.IOException +import java.io.StringReader +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.Future + +class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() { + private val collectionModel: CollectionViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = inflater.inflate(R.layout.journal_item_activity, container, false) + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + collectionModel.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name + if (container != null) { + initUi(inflater, ret, it) + } + } + } + + return ret + } + + private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) { + val viewPager = v.findViewById(R.id.viewpager) + viewPager.adapter = TabsAdapter(childFragmentManager, requireContext(), cachedCollection, cachedItem) + + val tabLayout = v.findViewById(R.id.tabs) + tabLayout.setupWithViewPager(viewPager) + + ListEntriesFragment.setItemView(v.findViewById(R.id.journal_list_item), cachedCollection.meta.collectionType, cachedItem) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.collection_item_fragment, menu) + // menu.setGroupVisible(R.id.journal_item_menu_event_invite, emailInvitationEvent != null) + } +} + +private class TabsAdapter(fm: FragmentManager, private val context: Context, private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : FragmentPagerAdapter(fm) { + + override fun getCount(): Int { + // FIXME: Make it depend on info enumType (only have non-raw for known types) + return 2 + } + + override fun getPageTitle(position: Int): CharSequence? { + return if (position == 0) { + context.getString(R.string.journal_item_tab_main) + } else { + context.getString(R.string.journal_item_tab_raw) + } + } + + override fun getItem(position: Int): Fragment { + return if (position == 0) { + PrettyFragment(cachedCollection, cachedItem.content) + } else { + TextFragment(cachedItem.content) + } + } +} + + +class TextFragment(private val content: String) : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.text_fragment, container, false) + + val tv = v.findViewById(R.id.content) as TextView + + tv.text = content + + return v + } +} + +class PrettyFragment(private val cachedCollection: CachedCollection, private val content: String) : Fragment() { + private var asyncTask: Future? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + var v: View? = null + + when (cachedCollection.meta.collectionType) { + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + v = inflater.inflate(R.layout.contact_info, container, false) + asyncTask = loadContactTask(v) + } + Constants.ETEBASE_TYPE_CALENDAR -> { + v = inflater.inflate(R.layout.event_info, container, false) + asyncTask = loadEventTask(v) + } + Constants.ETEBASE_TYPE_TASKS -> { + v = inflater.inflate(R.layout.task_info, container, false) + asyncTask = loadTaskTask(v) + } + } + + return v + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + private fun loadEventTask(view: View): Future { + return doAsync { + var event: Event? = null + val inputReader = StringReader(content) + + try { + event = Event.eventsFromReader(inputReader, null)[0] + } catch (e: InvalidCalendarException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + if (event != null) { + uiThread { + val loader = view.findViewById(R.id.event_info_loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.event_info_scroll_view) + contentContainer.visibility = View.VISIBLE + + setTextViewText(view, R.id.title, event.summary) + + setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context)) + + setTextViewText(view, R.id.where, event.location) + + val organizer = event.organizer + if (organizer != null) { + val tv = view.findViewById(R.id.organizer) as TextView + tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "") + } else { + val organizerView = view.findViewById(R.id.organizer_container) + organizerView.visibility = View.GONE + } + + setTextViewText(view, R.id.description, event.description) + + var first = true + var sb = StringBuilder() + for (attendee in event.attendees) { + if (first) { + first = false + sb.append(getString(R.string.journal_item_attendees)).append(": ") + } else { + sb.append(", ") + } + sb.append(attendee.calAddress.toString().replaceFirst("mailto:".toRegex(), "")) + } + setTextViewText(view, R.id.attendees, sb.toString()) + + first = true + sb = StringBuilder() + for (alarm in event.alarms) { + if (first) { + first = false + sb.append(getString(R.string.journal_item_reminders)).append(": ") + } else { + sb.append(", ") + } + sb.append(alarm.trigger.value) + } + setTextViewText(view, R.id.reminders, sb.toString()) + + /* FIXME: + if (event.attendees.isNotEmpty() && activity != null) { + (activity as JournalItemActivity).allowSendEmail(event, syncEntry.content) + } + */ + } + } + } + } + + private fun loadTaskTask(view: View): Future { + return doAsync { + var task: Task? = null + val inputReader = StringReader(content) + + try { + task = Task.tasksFromReader(inputReader)[0] + } catch (e: InvalidCalendarException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + if (task != null) { + uiThread { + val loader = view.findViewById(R.id.task_info_loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.task_info_scroll_view) + contentContainer.visibility = View.VISIBLE + + setTextViewText(view, R.id.title, task.summary) + + setTextViewText(view, R.id.where, task.location) + + val organizer = task.organizer + if (organizer != null) { + val tv = view.findViewById(R.id.organizer) as TextView + tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "") + } else { + val organizerView = view.findViewById(R.id.organizer_container) + organizerView.visibility = View.GONE + } + + setTextViewText(view, R.id.description, task.description) + } + } + } + } + + private fun loadContactTask(view: View): Future { + return doAsync { + var contact: Contact? = null + val reader = StringReader(content) + + try { + contact = Contact.fromReader(reader, null)[0] + } catch (e: IOException) { + e.printStackTrace() + } + + if (contact != null) { + uiThread { + val loader = view.findViewById(R.id.loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.content_container) + contentContainer.visibility = View.VISIBLE + + val tv = view.findViewById(R.id.display_name) as TextView + tv.text = contact.displayName + + if (contact.group) { + showGroup(contact) + } else { + showContact(contact) + } + } + } + } + } + + + private fun showGroup(contact: Contact) { + val view = requireView() + + val mainCard = view.findViewById(R.id.main_card) as ViewGroup + + addInfoItem(view.context, mainCard, getString(R.string.journal_item_member_count), null, contact.members.size.toString()) + + for (member in contact.members) { + addInfoItem(view.context, mainCard, getString(R.string.journal_item_member), null, member) + } + } + + + private fun showContact(contact: Contact) { + val view = requireView() + val mainCard = view.findViewById(R.id.main_card) as ViewGroup + val aboutCard = view.findViewById(R.id.about_card) as ViewGroup + aboutCard.findViewById(R.id.title_container).visibility = View.VISIBLE + + // TEL + for (labeledPhone in contact.phoneNumbers) { + val types = labeledPhone.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.text) + } + + // EMAIL + for (labeledEmail in contact.emails) { + val types = labeledEmail.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.value) + } + + // ORG, TITLE, ROLE + if (contact.organization != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0]) + } + if (contact.jobDescription != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle) + } + + // IMPP + for (labeledImpp in contact.impps) { + addInfoItem(view.context, mainCard, getString(R.string.journal_item_impp), labeledImpp.property.protocol, labeledImpp.property.handle) + } + + // NICKNAME + if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0]) + } + + // ADR + for (labeledAddress in contact.addresses) { + val types = labeledAddress.property.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.label) + } + + // NOTE + if (contact.note != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_note), null, contact.note) + } + + // URL + for (labeledUrl in contact.urls) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.value) + } + + // ANNIVERSARY + if (contact.anniversary != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate)) + } + // BDAY + if (contact.birthDay != null) { + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate)) + } + + // RELATED + for (related in contact.relations) { + val types = related.types + val type = if (types.size > 0) types[0].value else null + addInfoItem(view.context, aboutCard, getString(R.string.journal_item_relation), type, related.text) + } + + // PHOTO + // if (contact.photo != null) + } + + private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? { + if (date != null) { + val epochDate = date.time + return getDisplayedDatetime(epochDate, epochDate, true, context) + } else if (partialDate != null){ + val formatter = SimpleDateFormat("d MMMM", Locale.getDefault()) + val calendar = GregorianCalendar() + calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!) + calendar.set(Calendar.MONTH, partialDate.month!! - 1) + return formatter.format(calendar.time) + } + + return null + } + + companion object { + private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View { + val layout = parent.findViewById(R.id.container) as ViewGroup + val infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false) + layout.addView(infoItem) + setTextViewText(infoItem, R.id.type, type) + setTextViewText(infoItem, R.id.title, label) + setTextViewText(infoItem, R.id.content, value) + parent.visibility = View.VISIBLE + + return infoItem + } + + private fun setTextViewText(parent: View, id: Int, text: String?) { + val tv = parent.findViewById(id) as TextView + if (text == null) { + tv.visibility = View.GONE + } else { + tv.text = text + } + } + + fun getDisplayedDatetime(startMillis: Long, endMillis: Long, allDay: Boolean, context: Context?): String? { + // Configure date/time formatting. + val flagsDate = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY + var flagsTime = DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(context)) { + flagsTime = flagsTime or DateUtils.FORMAT_24HOUR + } + + val datetimeString: String + if (allDay) { + // For multi-day allday events or single-day all-day events that are not + // today or tomorrow, use framework formatter. + + // We need to remove 24hrs because full day events are from the start of a day until the start of the next + var adjustedEnd = endMillis - 24 * 60 * 60 * 1000; + if (adjustedEnd < startMillis) { + adjustedEnd = startMillis; + } + val f = Formatter(StringBuilder(50), Locale.getDefault()) + datetimeString = DateUtils.formatDateRange(context, f, startMillis, + adjustedEnd, flagsDate).toString() + } else { + // For multiday events, shorten day/month names. + // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" + val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or + DateUtils.FORMAT_ABBREV_WEEKDAY + datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis, + flagsDatetime) + } + return datetimeString + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt index 123ccabc..e6dde3ff 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.* import androidx.fragment.app.ListFragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.CachedItem import com.etesync.syncadapter.Constants @@ -60,8 +61,10 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val item = listAdapter?.getItem(position) as CachedItem - Toast.makeText(context, "Clicked ${item.item.uid}", Toast.LENGTH_LONG).show() - // startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content)) + activity?.supportFragmentManager?.commit { + replace(R.id.fragment_container, CollectionItemFragment(item)) + addToBackStack(EditCollectionFragment::class.java.name) + } } internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { diff --git a/app/src/main/res/menu/collection_item_fragment.xml b/app/src/main/res/menu/collection_item_fragment.xml new file mode 100644 index 00000000..2abd4b51 --- /dev/null +++ b/app/src/main/res/menu/collection_item_fragment.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + \ No newline at end of file From 251e610fe8d15eefdad2c7a224357c14903cfefb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 17:26:43 +0300 Subject: [PATCH 35/83] First sync failures on first account addition. --- .../main/java/com/etesync/syncadapter/EtebaseLocalCache.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index f3841cd6..036726ed 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -90,11 +90,11 @@ class EtebaseLocalCache private constructor(context: Context, username: String) fun collectionSet(colMgr: CollectionManager, collection: Collection) { val colDir = File(colsDir, collection.uid) - colDir.mkdir() + colDir.mkdirs() val colFile = File(colDir, "col") colFile.writeBytes(colMgr.cacheSaveWithContent(collection)) val itemsDir = getCollectionItemsDir(collection.uid) - itemsDir.mkdir() + itemsDir.mkdirs() } fun collectionUnset(colMgr: CollectionManager, colUid: String) { From df9f2f4ed465d1e3df1cec47e7d55f81e8c47157 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 18:05:44 +0300 Subject: [PATCH 36/83] Collection member listing. --- .../ui/etebase/CollectionMembersFragment.kt | 63 ++++++++ .../etebase/CollectionMembersListFragment.kt | 145 ++++++++++++++++++ .../ui/etebase/ViewCollectionFragment.kt | 15 +- .../etebase_view_collection_members.xml | 56 +++++++ 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt create mode 100644 app/src/main/res/layout/etebase_view_collection_members.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt new file mode 100644 index 00000000..013515a7 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt @@ -0,0 +1,63 @@ +package com.etesync.syncadapter.ui.etebase + +import android.graphics.Color.parseColor +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.ui.BaseActivity + +class CollectionMembersFragment : Fragment() { + private val collectionModel: CollectionViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = inflater.inflate(R.layout.etebase_view_collection_members, container, false) + + if (savedInstanceState == null) { + collectionModel.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.collection_members_title) + if (container != null) { + initUi(inflater, ret, it) + } + } + } + + return ret + } + + private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) { + v.findViewById(R.id.add_member).setOnClickListener { + + } + + val meta = cachedCollection.meta + val colorSquare = v.findViewById(R.id.color) + val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + when (meta.collectionType) { + Constants.ETEBASE_TYPE_CALENDAR -> { + colorSquare.setBackgroundColor(color) + } + Constants.ETEBASE_TYPE_TASKS -> { + colorSquare.setBackgroundColor(color) + } + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + colorSquare.visibility = View.GONE + } + } + + val title = v.findViewById(R.id.display_name) as TextView + title.text = meta.name + + val desc = v.findViewById(R.id.description) as TextView + desc.text = meta.description + + v.findViewById(R.id.progressBar).visibility = View.GONE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt new file mode 100644 index 00000000..b986c6d7 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt @@ -0,0 +1,145 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.fragment.app.ListFragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.observe +import com.etebase.client.CollectionMember +import com.etebase.client.FetchOptions +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.R +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.util.* +import java.util.concurrent.Future + +class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickListener { + private val model: AccountViewModel by activityViewModels() + private val collectionModel: CollectionViewModel by activityViewModels() + private val membersModel: LoadMembersViewModel by viewModels() + + private var emptyTextView: TextView? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.collection_members_list, container, false) + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = view.findViewById(android.R.id.empty) + return view + } + + private fun setListAdapterMembers(members: List) { + val context = context + if (context != null) { + val listAdapter = MembersListAdapter(context) + setListAdapter(listAdapter) + + listAdapter.addAll(members) + + emptyTextView!!.setText(R.string.collection_members_list_empty) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + model.observe(this) { + collectionModel.observe(this) { cachedCollection -> + membersModel.loadMembers(it, cachedCollection) + } + } + + membersModel.observe(this) { + setListAdapterMembers(it) + } + + listView.onItemClickListener = this + } + + override fun onDestroyView() { + super.onDestroyView() + + membersModel.cancelLoad() + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val member = listAdapter?.getItem(position) as CollectionMember + + /* + AlertDialog.Builder(requireActivity()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.collection_members_remove_title) + .setMessage(getString(R.string.collection_members_remove, member.user)) + .setPositiveButton(android.R.string.yes) { dialog, which -> + val frag = RemoveMemberFragment.newInstance(account, info, member.user!!) + frag.show(requireFragmentManager(), null) + } + .setNegativeButton(android.R.string.no) { dialog, which -> }.show() + */ + } + + internal inner class MembersListAdapter(context: Context) : ArrayAdapter(context, R.layout.collection_members_list_item) { + + override fun getView(position: Int, _v: View?, parent: ViewGroup): View { + var v = _v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.collection_members_list_item, parent, false) + + val member = getItem(position) + + val tv = v!!.findViewById(R.id.title) as TextView + tv.text = member!!.username + + // FIXME: Also mark admins + val readOnly = v.findViewById(R.id.read_only) + readOnly.visibility = if (member.accessLevel == "ro") View.VISIBLE else View.GONE + + return v + } + } +} + +class LoadMembersViewModel : ViewModel() { + private val members = MutableLiveData>() + private var asyncTask: Future? = null + + fun loadMembers(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) { + asyncTask = doAsync { + val ret = LinkedList() + val col = cachedCollection.col + val memberManager = accountCollectionHolder.colMgr.getMemberManager(col) + var iterator: String? = null + var done = false + while (!done) { + val chunk = memberManager.list(FetchOptions().iterator(iterator).limit(30)) + iterator = chunk.stoken + done = chunk.isDone + + ret.addAll(chunk.data) + } + + uiThread { + members.value = ret + } + } + } + + fun cancelLoad() { + asyncTask?.cancel(true) + } + + fun observe(owner: LifecycleOwner, observer: (List) -> Unit) = + members.observe(owner, observer) +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index eb27eb59..48ffcca6 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -129,8 +129,19 @@ class ViewCollectionFragment : Fragment() { } } R.id.on_manage_members -> { - Toast.makeText(context, "Manage", Toast.LENGTH_LONG).show() - } + if (cachedCollection.col.accessLevel == "adm") { + parentFragmentManager.commit { + replace(R.id.fragment_container, CollectionMembersFragment()) + addToBackStack(null) + } + } else { + val dialog = AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.edit_owner_only_anon) + .setPositiveButton(android.R.string.yes) { _, _ -> }.create() + dialog.show() + } } R.id.on_import -> { Toast.makeText(context, "Import", Toast.LENGTH_LONG).show() } diff --git a/app/src/main/res/layout/etebase_view_collection_members.xml b/app/src/main/res/layout/etebase_view_collection_members.xml new file mode 100644 index 00000000..7b3758f2 --- /dev/null +++ b/app/src/main/res/layout/etebase_view_collection_members.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + From cbe7e142dce5601e74325fbe65f77dc45b84cd7b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 13:34:09 +0300 Subject: [PATCH 37/83] Members: implement removing members. --- .../etebase/CollectionMembersListFragment.kt | 34 +++++++++++++++---- app/src/main/res/values/strings.xml | 1 + 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt index b986c6d7..df159325 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.ListFragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -27,7 +28,7 @@ import java.util.concurrent.Future class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickListener { private val model: AccountViewModel by activityViewModels() private val collectionModel: CollectionViewModel by activityViewModels() - private val membersModel: LoadMembersViewModel by viewModels() + private val membersModel: CollectionMembersViewModel by viewModels() private var emptyTextView: TextView? = null @@ -77,17 +78,23 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val member = listAdapter?.getItem(position) as CollectionMember - /* + if (member.accessLevel == "adm") { + AlertDialog.Builder(requireActivity()) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_remove_title) + .setMessage(R.string.collection_members_remove_admin) + .setNegativeButton(android.R.string.ok) { _, _ -> }.show() + return + } + AlertDialog.Builder(requireActivity()) .setIcon(R.drawable.ic_info_dark) .setTitle(R.string.collection_members_remove_title) - .setMessage(getString(R.string.collection_members_remove, member.user)) + .setMessage(getString(R.string.collection_members_remove, member.username)) .setPositiveButton(android.R.string.yes) { dialog, which -> - val frag = RemoveMemberFragment.newInstance(account, info, member.user!!) - frag.show(requireFragmentManager(), null) + membersModel.removeMember(model.value!!, collectionModel.value!!, member.username) } .setNegativeButton(android.R.string.no) { dialog, which -> }.show() - */ } internal inner class MembersListAdapter(context: Context) : ArrayAdapter(context, R.layout.collection_members_list_item) { @@ -111,7 +118,7 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis } } -class LoadMembersViewModel : ViewModel() { +class CollectionMembersViewModel : ViewModel() { private val members = MutableLiveData>() private var asyncTask: Future? = null @@ -136,6 +143,19 @@ class LoadMembersViewModel : ViewModel() { } } + fun removeMember(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, username: String) { + doAsync { + val col = cachedCollection.col + val memberManager = accountCollectionHolder.colMgr.getMemberManager(col) + memberManager.remove(username) + val ret = members.value!!.filter { it.username != username } + + uiThread { + members.value = ret + } + } + } + fun cancelLoad() { asyncTask?.cancel(true) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f43d977e..0aa671b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -166,6 +166,7 @@ Error removing member Remove member Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information. + Removing access to admins is currently not supported. About From c24936ff7efc9ac82b3d48f92e01cf36d00c6dca Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 14:01:49 +0300 Subject: [PATCH 38/83] Collections: implement inviting members. --- .../ui/etebase/CollectionMembersFragment.kt | 85 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt index 013515a7..721103cc 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt @@ -1,20 +1,32 @@ package com.etesync.syncadapter.ui.etebase +import android.app.Dialog +import android.app.ProgressDialog import android.graphics.Color.parseColor import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.EditText import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.etebase.client.Utils +import com.etebase.client.exceptions.EtebaseException +import com.etebase.client.exceptions.NotFoundException import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar import com.etesync.syncadapter.ui.BaseActivity +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread class CollectionMembersFragment : Fragment() { + private val model: AccountViewModel by activityViewModels() private val collectionModel: CollectionViewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -34,7 +46,7 @@ class CollectionMembersFragment : Fragment() { private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) { v.findViewById(R.id.add_member).setOnClickListener { - + addMemberClicked() } val meta = cachedCollection.meta @@ -60,4 +72,75 @@ class CollectionMembersFragment : Fragment() { v.findViewById(R.id.progressBar).visibility = View.GONE } + + private fun addMemberClicked() { + val view = View.inflate(requireContext(), R.layout.add_member_fragment, null) + val dialog = AlertDialog.Builder(requireContext()) + .setTitle(R.string.collection_members_add) + .setIcon(R.drawable.ic_account_add_dark) + .setPositiveButton(android.R.string.yes) { _, _ -> + val username = view.findViewById(R.id.username).text.toString() + val readOnly = view.findViewById(R.id.read_only).isChecked + + val frag = AddMemberFragment(model.value!!, collectionModel.value!!, username, if (readOnly) "ro" else "rw") + frag.show(childFragmentManager, null) + } + .setNegativeButton(android.R.string.no) { _, _ -> } + dialog.setView(view) + dialog.show() + } +} + +class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: String) : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val progress = ProgressDialog(context) + progress.setTitle(R.string.collection_members_adding) + progress.setMessage(getString(R.string.please_wait)) + progress.isIndeterminate = true + progress.setCanceledOnTouchOutside(false) + isCancelable = false + + doAsync { + val invitationManager = accountHolder.etebase.invitationManager + try { + val profile = invitationManager.fetchUserProfile(username) + val fingerprint = Utils.prettyFingerprint(profile.pubkey) + uiThread { + val view = LayoutInflater.from(context).inflate(R.layout.fingerprint_alertdialog, null) + (view.findViewById(R.id.body) as TextView).text = getString(R.string.trust_fingerprint_body, username) + (view.findViewById(R.id.fingerprint) as TextView).text = fingerprint + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_fingerprint_dark) + .setTitle(R.string.trust_fingerprint_title) + .setView(view) + .setPositiveButton(android.R.string.ok) { _, _ -> + doAsync { + try { + invitationManager.invite(cachedCollection.col, username, profile.pubkey, accessLevel) + uiThread { dismiss() } + } catch (e: EtebaseException) { + uiThread { handleError(e.localizedMessage) } + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }.show() + } + } catch (e: NotFoundException) { + uiThread { handleError(getString(R.string.collection_members_error_user_not_found, username)) } + } catch (e: EtebaseException) { + uiThread { handleError(e.localizedMessage) } + } + } + + return progress + } + + private fun handleError(message: String) { + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_add_error) + .setMessage(message) + .setPositiveButton(android.R.string.yes) { _, _ -> }.show() + dismiss() + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0aa671b3..667b7373 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,7 +161,7 @@ Adding member Verify security fingerprint Verify %s\'s security fingerprint to ensure the encryption is secure. - User (%s) not found. Have they setup their encryption password from one of the apps? + User (%s) not found Removing member Error removing member Remove member From 712346c7ae69a9c3a2c5be819141fba9b7901a37 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 15:19:50 +0300 Subject: [PATCH 39/83] Import: implement import in etebase. --- .../ui/etebase/ImportCollectionFragment.kt | 79 +++++++++++++++++++ .../ui/etebase/ViewCollectionFragment.kt | 16 +++- .../ui/importlocal/ImportActivity.kt | 9 +-- .../ui/importlocal/ImportFragment.kt | 60 ++++++++------ .../LocalCalendarImportFragment.kt | 30 +++---- .../importlocal/LocalContactImportFragment.kt | 28 +++---- .../ui/importlocal/ResultFragment.kt | 4 - 7 files changed, 151 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ImportCollectionFragment.kt diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ImportCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ImportCollectionFragment.kt new file mode 100644 index 00000000..14017fe9 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ImportCollectionFragment.kt @@ -0,0 +1,79 @@ +package com.etesync.syncadapter.ui.etebase + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.BaseActivity +import com.etesync.syncadapter.ui.importlocal.ImportFragment +import com.etesync.syncadapter.ui.importlocal.LocalCalendarImportFragment +import com.etesync.syncadapter.ui.importlocal.LocalContactImportFragment + +class ImportCollectionFragment : Fragment() { + private val model: AccountViewModel by activityViewModels() + private val collectionModel: CollectionViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = inflater.inflate(R.layout.import_actions_list, container, false) + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + collectionModel.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_dialog_title) + if (container != null) { + initUi(inflater, ret, it) + } + } + } + + return ret + } + + private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) { + val accountHolder = model.value!! + + var card = v.findViewById(R.id.import_file) + var img = card.findViewById(R.id.action_icon) as ImageView + var text = card.findViewById(R.id.action_text) as TextView + img.setImageResource(R.drawable.ic_file_white) + text.setText(R.string.import_button_file) + card.setOnClickListener { + parentFragmentManager.commit { + add(ImportFragment.newInstance(accountHolder.account, cachedCollection), null) + } + } + + card = v.findViewById(R.id.import_account) + img = card.findViewById(R.id.action_icon) as ImageView + text = card.findViewById(R.id.action_text) as TextView + img.setImageResource(R.drawable.ic_account_circle_white) + text.setText(R.string.import_button_local) + card.setOnClickListener { + if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR) { + parentFragmentManager.commit { + replace(R.id.fragment_container, LocalCalendarImportFragment(accountHolder.account, cachedCollection.col.uid)) + addToBackStack(null) + } + } else if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK) { + parentFragmentManager.commit { + replace(R.id.fragment_container, LocalContactImportFragment(accountHolder.account, cachedCollection.col.uid)) + addToBackStack(null) + } + } + // FIXME: should be in the fragments once we kill legacy + (activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_select_account) + } + + if (collectionModel.value!!.meta.collectionType == Constants.ETEBASE_TYPE_TASKS) { + card.visibility = View.GONE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index 48ffcca6..0d188f04 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -5,7 +5,6 @@ import android.graphics.Color.parseColor import android.os.Bundle import android.view.* import android.widget.TextView -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -15,7 +14,6 @@ import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar import com.etesync.syncadapter.ui.BaseActivity -import com.etesync.syncadapter.ui.EditCollectionActivity import com.etesync.syncadapter.ui.WebViewActivity import com.etesync.syncadapter.utils.HintManager import com.etesync.syncadapter.utils.ShowcaseBuilder @@ -143,7 +141,19 @@ class ViewCollectionFragment : Fragment() { dialog.show() } } R.id.on_import -> { - Toast.makeText(context, "Import", Toast.LENGTH_LONG).show() + if (cachedCollection.col.accessLevel != "ro") { + parentFragmentManager.commit { + replace(R.id.fragment_container, ImportCollectionFragment()) + addToBackStack(null) + } + } else { + val dialog = AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.edit_owner_only_anon) + .setPositiveButton(android.R.string.yes) { _, _ -> }.create() + dialog.show() + } } } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt index f7b22533..60a1398f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportActivity.kt @@ -14,7 +14,7 @@ import com.etesync.syncadapter.R import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.ui.BaseActivity -class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface { +class ImportActivity : BaseActivity(), SelectImportMethod, DialogInterface { private lateinit var account: Account protected lateinit var info: CollectionInfo @@ -83,13 +83,6 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo return super.onKeyDown(keyCode, event) } - override fun onImportResult(importResult: ResultFragment.ImportResult) { - val fragment = ResultFragment.newInstance(importResult) - supportFragmentManager.beginTransaction() - .add(fragment, "importResult") - .commitAllowingStateLoss() - } - override fun cancel() { finish() } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt index 5decaa76..6a3b5da8 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt @@ -14,12 +14,13 @@ import android.os.Bundle import android.provider.CalendarContract import android.provider.ContactsContract import androidx.fragment.app.DialogFragment +import androidx.fragment.app.commit import at.bitfire.ical4android.* import at.bitfire.vcard4android.BatchOperation import at.bitfire.vcard4android.Contact import at.bitfire.vcard4android.ContactsStorageException -import com.etesync.syncadapter.Constants.KEY_ACCOUNT -import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.Constants.* import com.etesync.syncadapter.R import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo @@ -34,19 +35,14 @@ import java.io.InputStream import java.io.InputStreamReader -class ImportFragment : DialogFragment() { +class ImportFragment(private val account: Account, private val uid: String, private val enumType: CollectionInfo.Type) : DialogFragment() { - private lateinit var account: Account - private lateinit var info: CollectionInfo private var inputStream: InputStream? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) isCancelable = false retainInstance = true - - account = arguments!!.getParcelable(KEY_ACCOUNT)!! - info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -56,7 +52,7 @@ class ImportFragment : DialogFragment() { } else { val data = ImportResult() data.e = Exception(getString(R.string.import_permission_required)) - (activity as ResultFragment.OnImportCallback).onImportResult(data) + onImportResult(data) dismissAllowingStateLoss() } @@ -117,7 +113,7 @@ class ImportFragment : DialogFragment() { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.action = Intent.ACTION_GET_CONTENT - when (info.enumType) { + when (enumType) { CollectionInfo.Type.CALENDAR -> intent.type = "text/calendar" CollectionInfo.Type.TASKS -> intent.type = "text/calendar" CollectionInfo.Type.ADDRESS_BOOK -> intent.type = "text/x-vcard" @@ -131,7 +127,7 @@ class ImportFragment : DialogFragment() { val data = ImportResult() data.e = Exception("Failed to open file chooser.\nPlease install one.") - (activity as ResultFragment.OnImportCallback).onImportResult(data) + onImportResult(data) dismissAllowingStateLoss() } @@ -145,7 +141,7 @@ class ImportFragment : DialogFragment() { if (data != null) { // Get the URI of the selected file val uri = data.data!! - Logger.log.info("Starting import into ${info.uid} from file ${uri}") + Logger.log.info("Starting import into ${uid} from file ${uri}") try { inputStream = activity!!.contentResolver.openInputStream(uri) @@ -156,7 +152,7 @@ class ImportFragment : DialogFragment() { val importResult = ImportResult() importResult.e = e - (activity as ResultFragment.OnImportCallback).onImportResult(importResult) + onImportResult(importResult) dismissAllowingStateLoss() } @@ -171,7 +167,7 @@ class ImportFragment : DialogFragment() { } fun loadFinished(data: ImportResult) { - (activity as ResultFragment.OnImportCallback).onImportResult(data) + onImportResult(data) Logger.log.info("Finished import") @@ -216,7 +212,7 @@ class ImportFragment : DialogFragment() { val context = context!! val importReader = InputStreamReader(inputStream) - if (info.enumType == CollectionInfo.Type.CALENDAR) { + if (enumType == CollectionInfo.Type.CALENDAR) { val events = Event.eventsFromReader(importReader, null) importReader.close() @@ -238,7 +234,7 @@ class ImportFragment : DialogFragment() { val localCalendar: LocalCalendar? try { - localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!) + localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, uid!!) if (localCalendar == null) { throw FileNotFoundException("Failed to load local resource.") } @@ -269,7 +265,7 @@ class ImportFragment : DialogFragment() { entryProcessed() } - } else if (info.enumType == CollectionInfo.Type.TASKS) { + } else if (enumType == CollectionInfo.Type.TASKS) { val tasks = Task.tasksFromReader(importReader) importReader.close() @@ -296,7 +292,7 @@ class ImportFragment : DialogFragment() { provider?.let { val localTaskList: LocalTaskList? try { - localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, info.uid!!) + localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, uid!!) if (localTaskList == null) { throw FileNotFoundException("Failed to load local resource.") } @@ -324,7 +320,7 @@ class ImportFragment : DialogFragment() { entryProcessed() } } - } else if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) { + } else if (enumType == CollectionInfo.Type.ADDRESS_BOOK) { val uidToLocalId = HashMap() val downloader = ContactsSyncManager.ResourceDownloader(context) val contacts = Contact.fromReader(importReader, downloader) @@ -345,7 +341,7 @@ class ImportFragment : DialogFragment() { return result } - val localAddressBook = LocalAddressBook.findByUid(context, provider, account, info.uid!!) + val localAddressBook = LocalAddressBook.findByUid(context, provider, account, uid!!) if (localAddressBook == null) { throw FileNotFoundException("Failed to load local address book.") } @@ -423,18 +419,30 @@ class ImportFragment : DialogFragment() { } } + fun onImportResult(importResult: ImportResult) { + val fragment = ResultFragment.newInstance(importResult) + parentFragmentManager.commit(true) { + add(fragment, "importResult") + } + } + companion object { private val REQUEST_CODE = 6384 // onActivityResult request private val TAG_PROGRESS_MAX = "progressMax" fun newInstance(account: Account, info: CollectionInfo): ImportFragment { - val frag = ImportFragment() - val args = Bundle(1) - args.putParcelable(KEY_ACCOUNT, account) - args.putSerializable(KEY_COLLECTION_INFO, info) - frag.arguments = args - return frag + return ImportFragment(account, info.uid!!, info.enumType!!) + } + + fun newInstance(account: Account, cachedCollection: CachedCollection): ImportFragment { + val enumType = when (cachedCollection.meta.collectionType) { + ETEBASE_TYPE_CALENDAR -> CollectionInfo.Type.CALENDAR + ETEBASE_TYPE_TASKS -> CollectionInfo.Type.TASKS + ETEBASE_TYPE_ADDRESS_BOOK -> CollectionInfo.Type.ADDRESS_BOOK + else -> throw Exception("Got unsupported collection type") + } + return ImportFragment(account, cachedCollection.col.uid, enumType) } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt index 6218dca8..92158852 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt @@ -14,9 +14,8 @@ import android.widget.ExpandableListView import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.ListFragment +import androidx.fragment.app.commit import at.bitfire.ical4android.CalendarStorageException -import com.etesync.syncadapter.Constants.KEY_ACCOUNT -import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO import com.etesync.syncadapter.R import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo @@ -24,17 +23,10 @@ import com.etesync.syncadapter.resource.LocalCalendar import com.etesync.syncadapter.resource.LocalEvent -class LocalCalendarImportFragment : ListFragment() { - - private lateinit var account: Account - private lateinit var info: CollectionInfo - +class LocalCalendarImportFragment(private val account: Account, private val uid: String) : ListFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true - - account = arguments!!.getParcelable(KEY_ACCOUNT)!! - info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -200,7 +192,7 @@ class LocalCalendarImportFragment : ListFragment() { if (progressDialog.isShowing && !activity.isDestroyed) { progressDialog.dismiss() } - (activity as ResultFragment.OnImportCallback).onImportResult(result) + onImportResult(result) } private fun importEvents(fromCalendar: LocalCalendar): ResultFragment.ImportResult { @@ -208,7 +200,7 @@ class LocalCalendarImportFragment : ListFragment() { try { val localCalendar = LocalCalendar.findByName(account, context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!, - LocalCalendar.Factory, info!!.uid!!) + LocalCalendar.Factory, uid) val localEvents = fromCalendar.findAll() val total = localEvents.size progressDialog.max = total @@ -248,15 +240,17 @@ class LocalCalendarImportFragment : ListFragment() { } } + fun onImportResult(importResult: ResultFragment.ImportResult) { + val fragment = ResultFragment.newInstance(importResult) + parentFragmentManager.commit(true) { + add(fragment, "importResult") + } + } + companion object { fun newInstance(account: Account, info: CollectionInfo): LocalCalendarImportFragment { - val frag = LocalCalendarImportFragment() - val args = Bundle(1) - args.putParcelable(KEY_ACCOUNT, account) - args.putSerializable(KEY_COLLECTION_INFO, info) - frag.arguments = args - return frag + return LocalCalendarImportFragment(account, info.uid!!) } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt index 079e0318..63916afb 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -18,6 +18,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import at.bitfire.vcard4android.ContactsStorageException @@ -32,18 +33,12 @@ import com.etesync.syncadapter.resource.LocalGroup import java.util.* -class LocalContactImportFragment : Fragment() { - - private lateinit var account: Account - private lateinit var info: CollectionInfo +class LocalContactImportFragment(private val account: Account, private val uid: String) : Fragment() { private var recyclerView: RecyclerView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true - - account = arguments!!.getParcelable(KEY_ACCOUNT) - info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -134,7 +129,7 @@ class LocalContactImportFragment : Fragment() { if (progressDialog.isShowing && !activity.isDestroyed) { progressDialog.dismiss() } - (activity as ResultFragment.OnImportCallback).onImportResult(result) + onImportResult(result) } private fun importContacts(localAddressBook: LocalAddressBook): ResultFragment.ImportResult { @@ -142,7 +137,7 @@ class LocalContactImportFragment : Fragment() { try { val addressBook = LocalAddressBook.findByUid(context!!, context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!, - account, info.uid!!)!! + account, uid)!! val localContacts = localAddressBook.findAllContacts() val localGroups = localAddressBook.findAllGroups() val oldIdToNewId = HashMap() @@ -214,6 +209,13 @@ class LocalContactImportFragment : Fragment() { } } + fun onImportResult(importResult: ResultFragment.ImportResult) { + val fragment = ResultFragment.newInstance(importResult) + parentFragmentManager.commit(true) { + add(fragment, "importResult") + } + } + class ImportContactAdapter /** * Initialize the dataset of the Adapter. @@ -316,13 +318,7 @@ class LocalContactImportFragment : Fragment() { companion object { fun newInstance(account: Account, info: CollectionInfo): LocalContactImportFragment { - val frag = LocalContactImportFragment() - val args = Bundle(1) - args.putParcelable(KEY_ACCOUNT, account) - args.putSerializable(KEY_COLLECTION_INFO, info) - frag.arguments = args - - return frag + return LocalContactImportFragment(account, info.uid!!) } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt index 1971b018..3f8b3694 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ResultFragment.kt @@ -72,10 +72,6 @@ class ResultFragment : DialogFragment() { } } - interface OnImportCallback { - fun onImportResult(importResult: ImportResult) - } - companion object { private val KEY_RESULT = "result" From 7ff80aaf9bd9977b1c273488043ed93e4e42863f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 16:17:34 +0300 Subject: [PATCH 40/83] SyncManager: fix crash with imported items. --- .../java/com/etesync/syncadapter/syncadapter/SyncManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index ab35b2e0..11539616 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -645,7 +645,7 @@ constructor(protected val context: Context, protected val account: Account, prot synchronized(etebaseLocalCache) { for (local in localDirty) { - val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!! else null + val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!) else null val item: Item if (cacheItem != null) { item = cacheItem.item From cccbfba568cacfed1537762968298c6a0b40e838 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 16:35:02 +0300 Subject: [PATCH 41/83] Contacts import: show a sensible error when address book is not found. --- .../ui/importlocal/LocalContactImportFragment.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt index 63916afb..8309ac65 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -22,8 +22,6 @@ import androidx.fragment.app.commit import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import at.bitfire.vcard4android.ContactsStorageException -import com.etesync.syncadapter.Constants.KEY_ACCOUNT -import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO import com.etesync.syncadapter.R import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo @@ -135,9 +133,10 @@ class LocalContactImportFragment(private val account: Account, private val uid: private fun importContacts(localAddressBook: LocalAddressBook): ResultFragment.ImportResult { val result = ResultFragment.ImportResult() try { - val addressBook = LocalAddressBook.findByUid(context!!, - context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!, - account, uid)!! + val addressBook = LocalAddressBook.findByUid(requireContext(), + requireContext().contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!, + account, uid) + ?: throw Exception("Could not find address book") val localContacts = localAddressBook.findAllContacts() val localGroups = localAddressBook.findAllGroups() val oldIdToNewId = HashMap() From a2a9a3e08c889e77fba0bbea97b98b4f738027ee Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 28 Aug 2020 16:46:14 +0300 Subject: [PATCH 42/83] Request sync after editing/removing a collection. --- .../syncadapter/ui/etebase/EditCollectionFragment.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt index 2647c5d1..5377704e 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt @@ -17,6 +17,7 @@ import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.resource.LocalCalendar +import com.etesync.syncadapter.syncadapter.requestSync import com.etesync.syncadapter.ui.BaseActivity import org.apache.commons.lang3.StringUtils import org.jetbrains.anko.doAsync @@ -156,6 +157,10 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri val col = cachedCollection.col col.delete() uploadCollection(col) + val applicationContext = activity?.applicationContext + if (applicationContext != null) { + requestSync(applicationContext, model.value!!.account) + } activity?.finish() } catch (e: EtebaseException) { uiThread { @@ -206,6 +211,10 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri val col = cachedCollection.col col.meta = meta uploadCollection(col) + val applicationContext = activity?.applicationContext + if (applicationContext != null) { + requestSync(applicationContext, model.value!!.account) + } if (isCreating) { parentFragmentManager.commit { replace(R.id.fragment_container, ViewCollectionFragment()) From 1bdd4d78f4960a1741fc3e78590e0206f7d0ce9b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sat, 29 Aug 2020 09:32:31 +0300 Subject: [PATCH 43/83] Implement account settings and password change. --- .../etesync/syncadapter/EtebaseLocalCache.kt | 1 + .../syncadapter/ui/AccountSettingsActivity.kt | 290 ++++++++++++------ .../ui/ChangeEncryptionPasswordActivity.kt | 42 ++- app/src/main/res/xml/settings_account.xml | 19 +- .../main/res/xml/settings_account_legacy.xml | 64 ++++ 5 files changed, 298 insertions(+), 118 deletions(-) create mode 100644 app/src/main/res/xml/settings_account_legacy.xml diff --git a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt index 036726ed..b51c2756 100644 --- a/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt +++ b/app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt @@ -153,6 +153,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String) } } + // FIXME: If we ever cache this we need to cache bust on changePassword fun getEtebase(context: Context, httpClient: OkHttpClient, settings: AccountSettings): Account { val client = Client.create(httpClient, settings.uri?.toString()) return Account.restore(client, settings.etebaseSession!!, null) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt index e190a5a5..ea8e8e3f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.kt @@ -18,11 +18,11 @@ import android.provider.CalendarContract import android.text.TextUtils import android.view.MenuItem import androidx.core.app.NavUtils +import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager import androidx.loader.content.AsyncTaskLoader import androidx.loader.content.Loader import androidx.preference.* -import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import com.etesync.syncadapter.* import com.etesync.syncadapter.Constants.KEY_ACCOUNT @@ -43,7 +43,8 @@ class AccountSettingsActivity : BaseActivity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) if (savedInstanceState == null) { - val frag = AccountSettingsFragment() + val settings = AccountSettings(this, account) + val frag: Fragment = if (settings.isLegacy) LegacyAccountSettingsFragment() else AccountSettingsFragment() frag.arguments = intent.extras supportFragmentManager.beginTransaction() .replace(android.R.id.content, frag) @@ -60,139 +61,226 @@ class AccountSettingsActivity : BaseActivity() { } else return false } +} - class AccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks { - internal lateinit var account: Account +class AccountSettingsFragment() : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks { + internal lateinit var account: Account - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - account = arguments?.getParcelable(KEY_ACCOUNT)!! + account = arguments?.getParcelable(KEY_ACCOUNT)!! - loaderManager.initLoader(0, arguments, this) - } + loaderManager.initLoader(0, arguments, this) + } - override fun onCreatePreferences(bundle: Bundle, s: String) { - addPreferencesFromResource(R.xml.settings_account) - } + override fun onCreatePreferences(bundle: Bundle, s: String) { + addPreferencesFromResource(R.xml.settings_account) + } - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) - } + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) + } - override fun onLoadFinished(loader: Loader, settings: AccountSettings?) { - if (settings == null) { - activity!!.finish() - return - } + override fun onLoadFinished(loader: Loader, settings: AccountSettings?) { + if (settings == null) { + activity!!.finish() + return + } + // Category: dashboard + val prefManageAccount = findPreference("manage_account") + prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> + WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build()) + true + } - // Category: dashboard - val prefManageAccount = findPreference("manage_account") - prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> - WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build()) - true - } + // Category: encryption + val prefEncryptionPassword = findPreference("password") + prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> + startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account)) + true + } - // category: authentication - val prefPassword = findPreference("password") as EditTextPreference - prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null - LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null) + val prefSync = findPreference("sync_interval") as ListPreference + val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval + if (syncInterval != null) { + prefSync.value = syncInterval.toString() + if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY) + prefSync.setSummary(R.string.settings_sync_summary_manually) + else + prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry) + prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val newInterval = java.lang.Long.parseLong(newValue as String) + settings.setSyncInterval(App.addressBooksAuthority, newInterval) + settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval) + OPENTASK_PROVIDERS.forEach { + settings.setSyncInterval(it.authority, newInterval) + } loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) false } + } else { + prefSync.isEnabled = false + prefSync.setSummary(R.string.settings_sync_summary_not_available) + } - // Category: encryption - val prefEncryptionPassword = findPreference("encryption_password") - prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> - startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account)) - true - } + val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat + prefWifiOnly.isChecked = settings.syncWifiOnly + prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly -> + settings.setSyncWiFiOnly(wifiOnly as Boolean) + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } - val prefSync = findPreference("sync_interval") as ListPreference - val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval - if (syncInterval != null) { - prefSync.value = syncInterval.toString() - if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY) - prefSync.setSummary(R.string.settings_sync_summary_manually) - else - prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry) - prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - val newInterval = java.lang.Long.parseLong(newValue as String) - settings.setSyncInterval(App.addressBooksAuthority, newInterval) - settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval) - OPENTASK_PROVIDERS.forEach { - settings.setSyncInterval(it.authority, newInterval) - } - loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) - false - } - } else { - prefSync.isEnabled = false - prefSync.setSummary(R.string.settings_sync_summary_not_available) - } + val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference + val onlySSID = settings.syncWifiOnlySSID + prefWifiOnlySSID.text = onlySSID + if (onlySSID != null) + prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID) + else + prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off) + prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val ssid = newValue as String + settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null + loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) + false + } + } - val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat - prefWifiOnly.isChecked = settings.syncWifiOnly - prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly -> - settings.setSyncWiFiOnly(wifiOnly as Boolean) - loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) - false - } + override fun onLoaderReset(loader: Loader) {} - val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference - val onlySSID = settings.syncWifiOnlySSID - prefWifiOnlySSID.text = onlySSID - if (onlySSID != null) - prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID) - else - prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off) - prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - val ssid = newValue as String - settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null - loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment) - false - } - } +} + + + +class LegacyAccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks { + internal lateinit var account: Account - override fun onLoaderReset(loader: Loader) {} + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + account = arguments?.getParcelable(KEY_ACCOUNT)!! + + loaderManager.initLoader(0, arguments, this) } + override fun onCreatePreferences(bundle: Bundle, s: String) { + addPreferencesFromResource(R.xml.settings_account_legacy) + } - private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader(context), SyncStatusObserver { - internal lateinit var listenerHandle: Any + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account) + } - override fun onStartLoading() { - forceLoad() - listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this) + override fun onLoadFinished(loader: Loader, settings: AccountSettings?) { + if (settings == null) { + activity!!.finish() + return } - override fun onStopLoading() { - ContentResolver.removeStatusChangeListener(listenerHandle) + // Category: dashboard + val prefManageAccount = findPreference("manage_account") + prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> + WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build()) + true } - override fun abandon() { - onStopLoading() + // category: authentication + val prefPassword = findPreference("password") as EditTextPreference + prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null + LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null) + loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment) + false } - override fun loadInBackground(): AccountSettings? { - val settings: AccountSettings - try { - settings = AccountSettings(context, account) - } catch (e: InvalidAccountException) { - return null - } + // Category: encryption + val prefEncryptionPassword = findPreference("encryption_password") + prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> + startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account)) + true + } - return settings + val prefSync = findPreference("sync_interval") as ListPreference + val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval + if (syncInterval != null) { + prefSync.value = syncInterval.toString() + if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY) + prefSync.setSummary(R.string.settings_sync_summary_manually) + else + prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry) + prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val newInterval = java.lang.Long.parseLong(newValue as String) + settings.setSyncInterval(App.addressBooksAuthority, newInterval) + settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval) + OPENTASK_PROVIDERS.forEach { + settings.setSyncInterval(it.authority, newInterval) + } + loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment) + false + } + } else { + prefSync.isEnabled = false + prefSync.setSummary(R.string.settings_sync_summary_not_available) } - override fun onStatusChanged(which: Int) { - Logger.log.fine("Reloading account settings") - forceLoad() + val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat + prefWifiOnly.isChecked = settings.syncWifiOnly + prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly -> + settings.setSyncWiFiOnly(wifiOnly as Boolean) + loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment) + false } + val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference + val onlySSID = settings.syncWifiOnlySSID + prefWifiOnlySSID.text = onlySSID + if (onlySSID != null) + prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID) + else + prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off) + prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val ssid = newValue as String + settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null + loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment) + false + } } + override fun onLoaderReset(loader: Loader) {} } + +private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader(context), SyncStatusObserver { + internal lateinit var listenerHandle: Any + + override fun onStartLoading() { + forceLoad() + listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this) + } + + override fun onStopLoading() { + ContentResolver.removeStatusChangeListener(listenerHandle) + } + + override fun abandon() { + onStopLoading() + } + + override fun loadInBackground(): AccountSettings? { + val settings: AccountSettings + try { + settings = AccountSettings(context, account) + } catch (e: InvalidAccountException) { + return null + } + + return settings + } + + override fun onStatusChanged(which: Int) { + Logger.log.fine("Reloading account settings") + forceLoad() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt index 983efee1..5b14cde2 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt @@ -15,6 +15,7 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog +import com.etebase.client.Client import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.HttpClient import com.etesync.syncadapter.R @@ -53,7 +54,7 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() { AlertDialog.Builder(this) .setTitle(R.string.wrong_encryption_password) .setIcon(R.drawable.ic_error_dark) - .setMessage(getString(R.string.wrong_encryption_password_content, e.localizedMessage)) + .setMessage(e.localizedMessage) .setPositiveButton(android.R.string.ok) { _, _ -> // dismiss }.show() @@ -62,6 +63,45 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() { fun changePasswordDo(old_password: String, new_password: String) { val settings = AccountSettings(this, account) + if (settings.isLegacy) { + legacyChangePasswordDo(settings, old_password, new_password) + return + } + + doAsync { + val httpClient = HttpClient.sharedClient + + try { + Logger.log.info("Loging in with old password") + val client = Client.create(httpClient, settings.uri?.toString()) + val etebase = com.etebase.client.Account.login(client, account.name, old_password) + Logger.log.info("Login successful") + + etebase.changePassword(new_password) + + settings.etebaseSession = etebase.save(null) + + uiThread { + progress.dismiss() + AlertDialog.Builder(this@ChangeEncryptionPasswordActivity) + .setTitle(R.string.change_encryption_password_success_title) + .setMessage(R.string.change_encryption_password_success_body) + .setPositiveButton(android.R.string.ok) { _, _ -> + this@ChangeEncryptionPasswordActivity.finish() + }.show() + + requestSync(applicationContext, account) + } + } catch (e: Exception) { + uiThread { + changePasswordError(e) + } + return@doAsync + } + } + } + + fun legacyChangePasswordDo(settings: AccountSettings, old_password: String, new_password: String) { doAsync { val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity, settings).setForeground(false).build().okHttpClient diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index fa996801..a8ffeddf 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -18,25 +18,12 @@ - - + - - - - - + /> + + + + + + + + + + + + + + + + + + + + + + + + + + From 3e7e90d46653cd02b1c6a1f34c56b44f49461982 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 31 Aug 2020 15:52:28 +0300 Subject: [PATCH 44/83] Server url: use our etebase partner URL. --- app/src/main/java/com/etesync/syncadapter/Constants.java | 1 + .../com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/Constants.java b/app/src/main/java/com/etesync/syncadapter/Constants.java index af2bf909..8dc1e370 100644 --- a/app/src/main/java/com/etesync/syncadapter/Constants.java +++ b/app/src/main/java/com/etesync/syncadapter/Constants.java @@ -35,6 +35,7 @@ public class Constants { public static final Uri forgotPassword = webUri.buildUpon().appendEncodedPath("accounts/password/reset/").build(); public static final Uri serviceUrl = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://api.etesync.com/" : DEBUG_REMOTE_URL); + public static final String etebaseServiceUrl = (DEBUG_REMOTE_URL == null) ? "https://api.etebase.com/partner/etesync/" : DEBUG_REMOTE_URL; public static final String PRODID_BASE = "-//EteSync//" + BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_NAME; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt index c9c44d04..4ece9937 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.kt @@ -90,7 +90,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred fun findInitialConfigurationEtebase(): Configuration { var exception: Throwable? = null - val uri = credentials.uri + val uri = credentials.uri ?: URI(Constants.etebaseServiceUrl) var etebaseSession: String? = null try { From d768ab69cd967bd5439fcfa628cb9f1e1095c9e3 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 1 Sep 2020 10:24:37 +0300 Subject: [PATCH 45/83] Fix parsing of #RRGGBBAA collection colors. --- .../syncadapter/resource/LocalCalendar.kt | 17 +++++++++++++++-- .../syncadapter/resource/LocalTaskList.kt | 4 +--- .../etesync/syncadapter/ui/AccountActivity.kt | 5 ++--- .../ui/etebase/CollectionMembersFragment.kt | 3 +-- .../ui/etebase/EditCollectionFragment.kt | 5 ++--- .../ui/etebase/ViewCollectionFragment.kt | 3 +-- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 8ce27a0d..4bfedc51 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -13,7 +13,6 @@ import android.content.ContentProviderClient import android.content.ContentProviderOperation import android.content.ContentUris import android.content.ContentValues -import android.graphics.Color.parseColor import android.net.Uri import android.os.RemoteException import android.provider.CalendarContract @@ -35,6 +34,20 @@ class LocalCalendar private constructor( companion object { val defaultColor = -0x743cb6 // light green 500 - should be "8BC349"? + fun parseColor(color_: String?): Int { + if (color_.isNullOrBlank()) { + return defaultColor + } + val color = color_.replaceFirst("^#".toRegex(), "") + if (color.length == 8) { + return (color.substring(0, 6).toLong(16) or (color.substring(6, 8).toLong(16) shl 24)).toInt() + } else if (color.length == 6) { + return (color.toLong(16) or (0xFF000000)).toInt() + } else { + return defaultColor + } + } + val COLUMN_CTAG = Calendars.CAL_SYNC1 fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri { @@ -111,7 +124,7 @@ class LocalCalendar private constructor( values.put(Calendars.CALENDAR_DISPLAY_NAME, meta.name) if (withColor) - values.put(Calendars.CALENDAR_COLOR, if (!meta.color.isNullOrBlank()) parseColor(meta.color) else defaultColor) + values.put(Calendars.CALENDAR_COLOR, parseColor(meta.color)) if (col.accessLevel == "ro") values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index dd716adb..10e7c344 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -11,11 +11,9 @@ package com.etesync.syncadapter.resource import android.accounts.Account import android.content.ContentValues import android.content.Context -import android.graphics.Color import android.net.Uri import android.os.Build import android.os.RemoteException -import android.provider.CalendarContract import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.AndroidTaskListFactory import at.bitfire.ical4android.CalendarStorageException @@ -89,7 +87,7 @@ class LocalTaskList private constructor( values.put(TaskLists.LIST_NAME, meta.name) if (withColor) - values.put(TaskLists.LIST_COLOR, if (!meta.color.isNullOrBlank()) Color.parseColor(meta.color) else defaultColor) + values.put(TaskLists.LIST_COLOR, LocalCalendar.parseColor(meta.color)) return values } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 6d3445b1..455b1fe9 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -13,7 +13,6 @@ import android.accounts.AccountManager import android.app.LoaderManager import android.content.* import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE -import android.graphics.Color.parseColor import android.net.Uri import android.os.Build import android.os.Bundle @@ -31,10 +30,10 @@ import at.bitfire.vcard4android.ContactsStorageException import com.etebase.client.CollectionManager import com.etebase.client.Utils import com.etebase.client.exceptions.EtebaseException -import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.JournalAuthenticator +import com.etesync.syncadapter.* import com.etesync.syncadapter.Constants.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo @@ -399,7 +398,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe val isAdmin = accessLevel == "adm" val metaColor = meta.color - val color = if (!metaColor.isNullOrBlank()) parseColor(metaColor) else null + val color = if (!metaColor.isNullOrBlank()) LocalCalendar.parseColor(metaColor) else null CollectionListItemInfo(it.col.uid, type, meta.name, meta.description ?: "", color, isReadOnly, isAdmin, null) }.filterNotNull() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt index 721103cc..f50bece7 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt @@ -2,7 +2,6 @@ package com.etesync.syncadapter.ui.etebase import android.app.Dialog import android.app.ProgressDialog -import android.graphics.Color.parseColor import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -51,7 +50,7 @@ class CollectionMembersFragment : Fragment() { val meta = cachedCollection.meta val colorSquare = v.findViewById(R.id.color) - val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + val color = LocalCalendar.parseColor(meta.color) when (meta.collectionType) { Constants.ETEBASE_TYPE_CALENDAR -> { colorSquare.setBackgroundColor(color) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt index 5377704e..c392f40d 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/EditCollectionFragment.kt @@ -1,6 +1,5 @@ package com.etesync.syncadapter.ui.etebase -import android.graphics.Color.parseColor import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextUtils @@ -79,7 +78,7 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri Constants.ETEBASE_TYPE_CALENDAR -> { title.setHint(R.string.create_calendar_display_name_hint) - val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + val color = LocalCalendar.parseColor(meta.color) colorSquare.setBackgroundColor(color) colorSquare.setOnClickListener { AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { @@ -94,7 +93,7 @@ class EditCollectionFragment(private val cachedCollection: CachedCollection, pri Constants.ETEBASE_TYPE_TASKS -> { title.setHint(R.string.create_tasklist_display_name_hint) - val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + val color = LocalCalendar.parseColor(meta.color) colorSquare.setBackgroundColor(color) colorSquare.setOnClickListener { AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index 0d188f04..89cdcf20 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -1,7 +1,6 @@ package com.etesync.syncadapter.ui.etebase import android.content.DialogInterface -import android.graphics.Color.parseColor import android.os.Bundle import android.view.* import android.widget.TextView @@ -67,7 +66,7 @@ class ViewCollectionFragment : Fragment() { val isAdmin = col.accessLevel == "adm" val colorSquare = container.findViewById(R.id.color) - val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor + val color = LocalCalendar.parseColor(meta.color) when (meta.collectionType) { Constants.ETEBASE_TYPE_CALENDAR -> { colorSquare.setBackgroundColor(color) From 73bebcd7c4fe191abbfe88589830cce66e2753b1 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 1 Sep 2020 13:14:00 +0300 Subject: [PATCH 46/83] Entries list: preserve scroll position when moving back to fragment. --- .../ui/etebase/ListEntriesFragment.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt index e6dde3ff..f5b78a09 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -2,10 +2,14 @@ package com.etesync.syncadapter.ui.etebase import android.content.Context import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView import androidx.fragment.app.ListFragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.commit @@ -16,10 +20,12 @@ import com.etesync.syncadapter.R import java.text.SimpleDateFormat import java.util.concurrent.Future + class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { private val collectionModel: CollectionViewModel by activityViewModels() private val itemsModel: ItemsViewModel by activityViewModels() private var asyncTask: Future? = null + private var state: Parcelable? = null private var emptyTextView: TextView? = null @@ -36,6 +42,8 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + var restored = false + collectionModel.observe(this) { col -> itemsModel.observe(this) { val entries = it.sortedByDescending { item -> @@ -46,12 +54,21 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { listAdapter.addAll(entries) + if(!restored && (state != null)) { + listView.onRestoreInstanceState(state) + restored = true + } + emptyTextView!!.text = getString(R.string.journal_entries_list_empty) } } listView.onItemClickListener = this } + override fun onPause() { + state = listView.onSaveInstanceState() + super.onPause() + } override fun onDestroyView() { super.onDestroyView() From 117f4e41f45339567e7452c8eac4752b9f056e01 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 1 Sep 2020 14:01:40 +0300 Subject: [PATCH 47/83] Item: support showing item revisions. --- .../ui/etebase/CollectionItemFragment.kt | 12 +- .../ui/etebase/ItemRevisionsListFragment.kt | 147 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ItemRevisionsListFragment.kt diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt index a42f4f14..dd2b8398 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt @@ -56,7 +56,7 @@ class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() { val tabLayout = v.findViewById(R.id.tabs) tabLayout.setupWithViewPager(viewPager) - ListEntriesFragment.setItemView(v.findViewById(R.id.journal_list_item), cachedCollection.meta.collectionType, cachedItem) + v.findViewById(R.id.journal_list_item).visibility = View.GONE } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -69,22 +69,26 @@ private class TabsAdapter(fm: FragmentManager, private val context: Context, pri override fun getCount(): Int { // FIXME: Make it depend on info enumType (only have non-raw for known types) - return 2 + return 3 } override fun getPageTitle(position: Int): CharSequence? { return if (position == 0) { context.getString(R.string.journal_item_tab_main) - } else { + } else if (position == 1) { context.getString(R.string.journal_item_tab_raw) + } else { + context.getString(R.string.journal_item_tab_revisions) } } override fun getItem(position: Int): Fragment { return if (position == 0) { PrettyFragment(cachedCollection, cachedItem.content) - } else { + } else if (position == 1) { TextFragment(cachedItem.content) + } else { + ItemRevisionsListFragment(cachedCollection, cachedItem) } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ItemRevisionsListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ItemRevisionsListFragment.kt new file mode 100644 index 00000000..4f4ee7a9 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ItemRevisionsListFragment.kt @@ -0,0 +1,147 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.fragment.app.ListFragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit +import androidx.fragment.app.viewModels +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.observe +import com.etebase.client.FetchOptions +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.CachedItem +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.etebase.ListEntriesFragment.Companion.setItemView +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.util.* +import java.util.concurrent.Future + + +class ItemRevisionsListFragment(private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : ListFragment(), AdapterView.OnItemClickListener { + private val model: AccountViewModel by activityViewModels() + private val revisionsModel: RevisionsViewModel by viewModels() + private var state: Parcelable? = null + + private var emptyTextView: TextView? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.journal_viewer_list, container, false) + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = view.findViewById(android.R.id.empty) as TextView + + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + var restored = false + + revisionsModel.loadRevisions(model.value!!, cachedCollection, cachedItem) + revisionsModel.observe(this) { + val entries = it.sortedByDescending { item -> + item.meta.mtime ?: 0 + } + val listAdapter = EntriesListAdapter(requireContext(), cachedCollection) + setListAdapter(listAdapter) + + listAdapter.addAll(entries) + + if(!restored && (state != null)) { + listView.onRestoreInstanceState(state) + restored = true + } + + emptyTextView!!.text = getString(R.string.journal_entries_list_empty) + } + + listView.onItemClickListener = this + } + override fun onPause() { + state = listView.onSaveInstanceState() + super.onPause() + } + + override fun onDestroyView() { + super.onDestroyView() + revisionsModel.cancelLoad() + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val item = listAdapter?.getItem(position) as CachedItem + activity?.supportFragmentManager?.commit { + replace(R.id.fragment_container, CollectionItemFragment(item)) + addToBackStack(null) + } + } + + internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { + + override fun getView(position: Int, _v: View?, parent: ViewGroup): View { + var v = _v + if (v == null) + v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!! + + val item = getItem(position) + + setItemView(v, cachedCollection.meta.collectionType, item) + + /* FIXME: handle entry error: + val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull() + if (entryError != null) { + val errorIcon = v.findViewById(R.id.error) as ImageView + errorIcon.visibility = View.VISIBLE + } + */ + + return v + } + } +} + + +class RevisionsViewModel : ViewModel() { + private val revisions = MutableLiveData>() + private var asyncTask: Future? = null + + fun loadRevisions(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, cachedItem: CachedItem) { + asyncTask = doAsync { + val ret = LinkedList() + val col = cachedCollection.col + val itemManager = accountCollectionHolder.colMgr.getItemManager(col) + var iterator: String? = null + var done = false + while (!done) { + val chunk = itemManager.itemRevisions(cachedItem.item, FetchOptions().iterator(iterator).limit(30)) + iterator = chunk.iterator + done = chunk.isDone + + ret.addAll(chunk.data.map { CachedItem(it, it.meta, it.contentString) }) + } + + uiThread { + revisions.value = ret + } + } + } + + fun cancelLoad() { + asyncTask?.cancel(true) + } + + fun observe(owner: LifecycleOwner, observer: (List) -> Unit) = + revisions.observe(owner, observer) +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 667b7373..388ff1c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -171,6 +171,7 @@ About Main + Revisions Raw Attendees Reminders From f725b3069b85974214da9a8afe5cf90fb908f25f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 1 Sep 2020 14:03:07 +0300 Subject: [PATCH 48/83] List entries fragment: remove unused variable. --- .../etesync/syncadapter/ui/etebase/ListEntriesFragment.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt index f5b78a09..62bc36b0 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -18,13 +18,11 @@ import com.etesync.syncadapter.CachedItem import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import java.text.SimpleDateFormat -import java.util.concurrent.Future class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { private val collectionModel: CollectionViewModel by activityViewModels() private val itemsModel: ItemsViewModel by activityViewModels() - private var asyncTask: Future? = null private var state: Parcelable? = null private var emptyTextView: TextView? = null @@ -70,12 +68,6 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { super.onPause() } - override fun onDestroyView() { - super.onDestroyView() - if (asyncTask != null) - asyncTask!!.cancel(true) - } - override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val item = listAdapter?.getItem(position) as CachedItem activity?.supportFragmentManager?.commit { From 5f8ca4326bb463ef26f9871773ab9be917741732 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 1 Sep 2020 14:25:38 +0300 Subject: [PATCH 49/83] Bump version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8537caed..8c0bddaf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { minSdkVersion 19 targetSdkVersion 29 - versionCode 114 - versionName "1.16.2" + versionCode 200 + versionName "2.0.0" buildConfigField "boolean", "customCerts", "true" } From 2417f77a39d9de76d8e60b61aa38cb77c26643ab Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 3 Sep 2020 08:32:39 +0300 Subject: [PATCH 50/83] Sync: handle permission denied sync errors. --- .../syncadapter/SyncNotification.kt | 31 +++++++++++-------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt index 11f55848..0c478331 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncNotification.kt @@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import at.bitfire.ical4android.CalendarStorageException import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.exceptions.* import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R @@ -27,7 +28,8 @@ class SyncNotification(internal val context: Context, internal val notificationT internal val notificationManager: NotificationManagerCompat lateinit var detailsIntent: Intent internal set - internal var messageString: Int = 0 + internal var messageInt: Int = 0 + internal var messageString: String? = null private var throwable: Throwable? = null @@ -37,30 +39,33 @@ class SyncNotification(internal val context: Context, internal val notificationT fun setThrowable(e: Throwable) { throwable = e - if (e is Exceptions.UnauthorizedException) { + if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) { Logger.log.log(Level.SEVERE, "Not authorized anymore", e) - messageString = R.string.sync_error_unauthorized + messageInt = R.string.sync_error_unauthorized } else if (e is Exceptions.UserInactiveException) { Logger.log.log(Level.SEVERE, "User inactive") - messageString = R.string.sync_error_user_inactive - } else if (e is Exceptions.ServiceUnavailableException) { + messageInt = R.string.sync_error_user_inactive + } else if (e is Exceptions.ServiceUnavailableException || e is TemporaryServerErrorException) { Logger.log.log(Level.SEVERE, "Service unavailable") - messageString = R.string.sync_error_unavailable + messageInt = R.string.sync_error_unavailable } else if (e is Exceptions.ReadOnlyException) { Logger.log.log(Level.SEVERE, "Journal is read only", e) - messageString = R.string.sync_error_journal_readonly - } else if (e is Exceptions.HttpException) { + messageInt = R.string.sync_error_journal_readonly + } else if (e is PermissionDeniedException) { + Logger.log.log(Level.SEVERE, "Permission denied", e) + messageString = context.getString(R.string.sync_error_permission_denied, e.localizedMessage) + } else if (e is Exceptions.HttpException || e is ServerErrorException) { Logger.log.log(Level.SEVERE, "HTTP Exception during sync", e) - messageString = R.string.sync_error_http_dav + messageInt = R.string.sync_error_http_dav } else if (e is CalendarStorageException || e is ContactsStorageException || e is SQLiteException) { Logger.log.log(Level.SEVERE, "Couldn't access local storage", e) - messageString = R.string.sync_error_local_storage + messageInt = R.string.sync_error_local_storage } else if (e is Exceptions.IntegrityException) { Logger.log.log(Level.SEVERE, "Integrity error", e) - messageString = R.string.sync_error_integrity + messageInt = R.string.sync_error_integrity } else { Logger.log.log(Level.SEVERE, "Unknown sync error", e) - messageString = R.string.sync_error + messageInt = R.string.sync_error } detailsIntent = Intent(context, NotificationHandlerActivity::class.java) @@ -69,7 +74,7 @@ class SyncNotification(internal val context: Context, internal val notificationT } fun notify(title: String, state: String) { - val message = context.getString(messageString, state) + val message = messageString ?: context.getString(messageInt, state) notify(title, message, null, detailsIntent) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 388ff1c6..49d5e0b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -359,6 +359,7 @@ Could not connect to server while %s Database error while %s Journal is read only + Permission denied: %s preparing synchronization syncronizing journals preparing for fetch From f6a44a33da283a1b7f4989e279e7952829c2fa4f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 3 Sep 2020 09:13:48 +0300 Subject: [PATCH 51/83] AccountActivity: fix crash when removing account. --- .../java/com/etesync/syncadapter/ui/AccountActivity.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 455b1fe9..55659693 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -407,7 +407,12 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe override fun loadInBackground(): AccountInfo { val info = AccountInfo() - val settings = AccountSettings(context, account) + val settings: AccountSettings + try { + settings = AccountSettings(context, account) + } catch (e: InvalidAccountException) { + return info + } if (settings.isLegacy) { val data = (context.applicationContext as App).data From 671045917691c610e4f8080bbd9e6a13b41329fb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sat, 5 Sep 2020 13:01:31 +0300 Subject: [PATCH 52/83] SyncManager tasks: fix bug with task lists being reset. --- .../etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt index e1b31e9e..8059fb47 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt @@ -99,7 +99,7 @@ class TasksSyncAdapterService: SyncAdapterService() { // delete obsolete local calendar for (taskList in local) { - val url = taskList.name + val url = taskList.syncId val collection = remote[url] if (collection == null) { Logger.log.fine("Deleting obsolete local taskList $url") From 6459d71ab66517a6857141dd8bc06803292a3ce4 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Sep 2020 09:47:37 +0300 Subject: [PATCH 53/83] Make sure we never upload items without a uuid. --- .../java/com/etesync/syncadapter/syncadapter/SyncManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 11539616..4db67cdf 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -652,7 +652,7 @@ constructor(protected val context: Context, protected val account: Account, prot itemUpdateMtime(item) } else { val meta = ItemMetadata() - meta.name = local.uuid + meta.name = local.uuid!! meta.setMtime(System.currentTimeMillis()) item = itemMgr.create(meta, "") From 00a1a223d050c0820bac03628f55ceff2cbc043b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Sep 2020 10:16:01 +0300 Subject: [PATCH 54/83] SyncManager: correctly set the item's UID in the metadata. We were only populating the uid after setting it in the metadata so we were always setting null in the metadata which was causing errors. --- .../com/etesync/syncadapter/resource/LocalContact.kt | 12 +++++++++++- .../com/etesync/syncadapter/resource/LocalEvent.kt | 12 +++++++++++- .../com/etesync/syncadapter/resource/LocalGroup.kt | 12 +++++++++++- .../etesync/syncadapter/resource/LocalResource.kt | 5 +++-- .../com/etesync/syncadapter/resource/LocalTask.kt | 12 +++++++++++- .../etesync/syncadapter/syncadapter/SyncManager.kt | 11 ++++++----- 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt index 797eb1b5..b6c9d6a3 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalContact.kt @@ -107,7 +107,7 @@ class LocalContact : AndroidContact, LocalAddress { this.eTag = eTag } - override fun prepareForUpload(fileName_: String?) { + override fun legacyPrepareForUpload(fileName_: String?) { val uid = UUID.randomUUID().toString() val values = ContentValues(2) @@ -119,6 +119,16 @@ class LocalContact : AndroidContact, LocalAddress { this.fileName = fileName } + override fun prepareForUpload(fileName: String, uid: String) { + val values = ContentValues(2) + values.put(AndroidContact.COLUMN_FILENAME, fileName) + values.put(AndroidContact.COLUMN_UID, uid) + addressBook.provider?.update(rawContactSyncURI(), values, null, null) + + contact?.uid = uid + this.fileName = fileName + } + override fun populateData(mimeType: String, row: ContentValues) { when (mimeType) { CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID)) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt index ef39ec2e..93cd2fbc 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalEvent.kt @@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource { /* custom queries */ - override fun prepareForUpload(fileName_: String?) { + override fun legacyPrepareForUpload(fileName_: String?) { var uid: String? = null val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) if (c.moveToNext()) @@ -156,6 +156,16 @@ class LocalEvent : AndroidEvent, LocalResource { event.uid = uid } + override fun prepareForUpload(fileName: String, uid: String) { + val values = ContentValues(2) + values.put(Events._SYNC_ID, fileName) + values.put(COLUMN_UID, uid) + calendar.provider.update(eventSyncURI(), values, null, null) + + event?.uid = uid + this.fileName = fileName + } + override fun resetDeleted() { val values = ContentValues(1) values.put(CalendarContract.Events.DELETED, 0) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index 2f8192ed..62ff13cd 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -156,7 +156,7 @@ class LocalGroup : AndroidGroup, LocalAddress { batch.commit() } - override fun prepareForUpload(fileName_: String?) { + override fun legacyPrepareForUpload(fileName_: String?) { val uid = UUID.randomUUID().toString() val values = ContentValues(2) @@ -168,6 +168,16 @@ class LocalGroup : AndroidGroup, LocalAddress { this.fileName = fileName } + override fun prepareForUpload(fileName: String, uid: String) { + val values = ContentValues(2) + values.put(AndroidGroup.COLUMN_FILENAME, fileName) + values.put(AndroidGroup.COLUMN_UID, uid) + addressBook.provider?.update(groupSyncUri(), values, null, null) + + contact?.uid = uid + this.fileName = fileName + } + override fun resetDeleted() { val values = ContentValues(1) values.put(Groups.DELETED, 0) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt index 92d77185..38aa6b9e 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalResource.kt @@ -20,8 +20,9 @@ interface LocalResource { fun delete(): Int - // FIXME: The null is for legacy - fun prepareForUpload(fileName: String?) + fun legacyPrepareForUpload(fileName: String?) + + fun prepareForUpload(fileName: String, uid: String) fun clearDirty(eTag: String?) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index 1aa3b4c8..d39d8d4b 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -96,7 +96,7 @@ class LocalTask : AndroidTask, LocalResource { /* custom queries */ - override fun prepareForUpload(fileName_: String?) { + override fun legacyPrepareForUpload(fileName_: String?) { var uid: String? = null val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null) if (c.moveToNext()) @@ -118,6 +118,16 @@ class LocalTask : AndroidTask, LocalResource { task.uid = uid } + override fun prepareForUpload(fileName: String, uid: String) { + val values = ContentValues(2) + values.put(TaskContract.Tasks._SYNC_ID, fileName) + values.put(COLUMN_UID, uid) + taskList.provider.client.update(taskSyncURI(), values, null, null) + + task?.uid = uid + this.fileName = fileName + } + override fun resetDeleted() { val values = ContentValues(1) values.put(TaskContract.Tasks._DELETED, 0) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 4db67cdf..6dcca0e9 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -651,12 +651,13 @@ constructor(protected val context: Context, protected val account: Account, prot item = cacheItem.item itemUpdateMtime(item) } else { + val uid = UUID.randomUUID().toString() val meta = ItemMetadata() - meta.name = local.uuid!! - meta.setMtime(System.currentTimeMillis()) + meta.name = uid + meta.mtime = System.currentTimeMillis() item = itemMgr.create(meta, "") - local.prepareForUpload(item.uid) + local.prepareForUpload(item.uid, uid) } try { @@ -773,7 +774,7 @@ constructor(protected val context: Context, protected val account: Account, prot if (isLegacy) { // It's done later for non-legacy Logger.log.fine("Entry deleted before ever syncing - genarting a UUID") - local.prepareForUpload(null) + local.legacyPrepareForUpload(null) } } @@ -812,7 +813,7 @@ constructor(protected val context: Context, protected val account: Account, prot } Logger.log.fine("Found local record without file name; generating file name/UID if necessary") - local.prepareForUpload(null) + local.legacyPrepareForUpload(null) } } } From 97d1a40e490d9828b736df17b72dd1a9af7c2c61 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Sep 2020 10:20:27 +0300 Subject: [PATCH 55/83] AddressBook: set isSyncable=1 on creation and update readOnly Set isSyncable=1 at creation (and not only after first sync) --- .../java/com/etesync/syncadapter/resource/LocalAddressBook.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index b33919b6..9157ff57 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -91,12 +91,14 @@ class LocalAddressBook( val addressBook = LocalAddressBook(context, account, provider) + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1) ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true) val values = ContentValues(2) values.put(ContactsContract.Settings.SHOULD_SYNC, 1) values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) addressBook.settings = values + addressBook.readOnly = col.accessLevel == "ro" return addressBook } From bc44062e936137668b53dd9f07d411eb50c585ed Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Sep 2020 10:51:41 +0300 Subject: [PATCH 56/83] Local resources: fix the mess of find by username/uid being all mixed up. We were searching by filename but the function was called uid and other such mixups. This change should now fix all of them by having a function for each that actually does the right thing. --- .../etesync/syncadapter/resource/LocalAddressBook.kt | 12 +++++++++++- .../etesync/syncadapter/resource/LocalCalendar.kt | 9 +++++++-- .../etesync/syncadapter/resource/LocalCollection.kt | 3 ++- .../com/etesync/syncadapter/resource/LocalGroup.kt | 2 +- .../etesync/syncadapter/resource/LocalTaskList.kt | 7 +++++-- .../syncadapter/syncadapter/CalendarSyncManager.kt | 2 +- .../syncadapter/syncadapter/ContactsSyncManager.kt | 9 +++------ .../syncadapter/syncadapter/TasksSyncManager.kt | 2 +- .../etesync/syncadapter/ui/JournalItemActivity.kt | 6 +++--- .../syncadapter/ui/importlocal/ImportFragment.kt | 8 ++++---- .../ui/importlocal/LocalCalendarImportFragment.kt | 2 +- .../ui/importlocal/LocalContactImportFragment.kt | 4 ++-- 12 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index 9157ff57..a3aac76a 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -346,7 +346,7 @@ class LocalAddressBook( return reallyDirty } - override fun findByFilename(uid: String): LocalAddress? { + override fun findByUid(uid: String): LocalAddress? { val found = findContactByUID(uid) if (found != null) { return found @@ -355,6 +355,16 @@ class LocalAddressBook( } } + override fun findByFilename(filename: String): LocalAddress? { + val found = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull() + + if (found != null) { + return found + } else { + return queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull() + } + } + fun findGroupById(id: Long): LocalGroup = queryGroups("${Groups._ID}=?", arrayOf(id.toString())).firstOrNull() ?: throw FileNotFoundException() diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 4bfedc51..92142847 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -21,7 +21,9 @@ import at.bitfire.ical4android.* import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.resource.LocalEvent.Companion.COLUMN_UID import org.apache.commons.lang3.StringUtils +import org.dmfs.tasks.contract.TaskContract import java.util.* import java.util.logging.Level @@ -178,8 +180,11 @@ class LocalCalendar private constructor( override fun findAll(): List = queryEvents(null, null) - override fun findByFilename(uid: String): LocalEvent? - = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() + override fun findByUid(uid: String): LocalEvent? + = queryEvents(COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull() + + override fun findByFilename(filename: String): LocalEvent? + = queryEvents(Events._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull() fun processDirtyExceptions() { // process deleted exceptions diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt index 5a9b5340..1bcee902 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -16,7 +16,8 @@ interface LocalCollection> { fun findWithoutFileName(): List fun findAll(): List - fun findByFilename(uid: String): T? + fun findByUid(uid: String): T? + fun findByFilename(filename: String): T? fun count(): Long diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt index 62ff13cd..6af2801a 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalGroup.kt @@ -63,7 +63,7 @@ class LocalGroup : AndroidGroup, LocalAddress { // insert memberships val membersIds = members.map {uid -> Constants.log.fine("Assigning member: $uid") - val contact = addressBook.findByFilename(uid) as LocalContact? + val contact = addressBook.findByUid(uid) as LocalContact? if (contact != null) contact.id else null }.filterNotNull() diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index 10e7c344..251f9506 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -124,8 +124,11 @@ class LocalTaskList private constructor( override fun findWithoutFileName(): List = queryTasks(Tasks._SYNC_ID + " IS NULL", null) - override fun findByFilename(uid: String): LocalTask? - = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() + override fun findByUid(uid: String): LocalTask? + = queryTasks(LocalTask.COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull() + + override fun findByFilename(filename: String): LocalTask? + = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull() override fun count(): Long { try { diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt index 2ef6ed3f..7592b8b2 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.kt @@ -119,7 +119,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } val event = events[0] - val local = localCollection!!.findByFilename(event.uid!!) + val local = localCollection!!.findByUid(event.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { legacyProcessEvent(event, local) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt index ee17b13f..e58f864b 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.kt @@ -13,7 +13,6 @@ import android.content.* import android.os.Bundle import android.provider.ContactsContract import at.bitfire.ical4android.CalendarStorageException -import at.bitfire.ical4android.Event import at.bitfire.vcard4android.BatchOperation import at.bitfire.vcard4android.Contact import at.bitfire.vcard4android.ContactsStorageException @@ -132,9 +131,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra } override fun processItem(item: Item) { - val uid = item.meta.name!! - - val local = localCollection!!.findByFilename(uid) + val local = localCollection!!.findByFilename(item.uid) if (!item.isDeleted) { val inputReader = StringReader(String(item.content)) @@ -154,7 +151,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra Logger.log.info("Removing local record which has been deleted on the server") local.delete() } else { - Logger.log.warning("Tried deleting a non-existent record: " + uid) + Logger.log.warning("Tried deleting a non-existent record: " + item.uid) } } } @@ -171,7 +168,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra Logger.log.warning("Received multiple VCards, using first one") val contact = contacts[0] - val local = localCollection!!.findByFilename(contact.uid!!) + val local = localCollection!!.findByUid(contact.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { legacyProcessContact(contact, local) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt index f0b1a8de..51643537 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt @@ -109,7 +109,7 @@ class TasksSyncManager( } val event = tasks[0] - val local = localCollection!!.findByFilename(event.uid!!) + val local = localCollection!!.findByUid(event.uid!!) if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { legacyProcessTask(event, local) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt index 0a6fc2c6..2f4a18bc 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt @@ -108,7 +108,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { val provider = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!! val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)!! val event = Event.eventsFromReader(StringReader(syncEntry.content))[0] - var localEvent = localCalendar.findByFilename(event.uid!!) + var localEvent = localCalendar.findByUid(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event) } else { @@ -121,7 +121,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { val provider = TaskProvider.acquire(this, it)!! val localTaskList = LocalTaskList.findByName(account, provider, LocalTaskList.Factory, info.uid!!)!! val task = Task.tasksFromReader(StringReader(syncEntry.content))[0] - var localTask = localTaskList.findByFilename(task.uid!!) + var localTask = localTaskList.findByUid(task.uid!!) if (localTask != null) { localTask.updateAsDirty(task) } else { @@ -137,7 +137,7 @@ class JournalItemActivity : BaseActivity(), Refreshable { if (contact.group) { // FIXME: not currently supported } else { - var localContact = localAddressBook.findByFilename(contact.uid!!) as LocalContact? + var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) } else { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt index 6a3b5da8..c2bef3c9 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt @@ -250,7 +250,7 @@ class ImportFragment(private val account: Account, private val uid: String, priv for (event in events) { try { - var localEvent = localCalendar.findByFilename(event.uid!!) + var localEvent = localCalendar.findByUid(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event) result.updated++ @@ -304,7 +304,7 @@ class ImportFragment(private val account: Account, private val uid: String, priv for (task in tasks) { try { - var localTask = localTaskList.findByFilename(task.uid!!) + var localTask = localTaskList.findByUid(task.uid!!) if (localTask != null) { localTask.updateAsDirty(task) result.updated++ @@ -348,7 +348,7 @@ class ImportFragment(private val account: Account, private val uid: String, priv for (contact in contacts.filter { contact -> !contact.group }) { try { - var localContact = localAddressBook.findByFilename(contact.uid!!) as LocalContact? + var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) @@ -381,7 +381,7 @@ class ImportFragment(private val account: Account, private val uid: String, priv } val group = contact - var localGroup: LocalGroup? = localAddressBook.findByFilename(group.uid!!) as LocalGroup? + var localGroup: LocalGroup? = localAddressBook.findByUid(group.uid!!) as LocalGroup? if (localGroup != null) { localGroup.updateAsDirty(group, memberIds) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt index 92158852..094360a9 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalCalendarImportFragment.kt @@ -214,7 +214,7 @@ class LocalCalendarImportFragment(private val account: Account, private val uid: var localEvent = if (event == null || event.uid == null) null else - localCalendar.findByFilename(event.uid!!) + localCalendar.findByUid(event.uid!!) if (localEvent != null) { localEvent.updateAsDirty(event!!) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt index 8309ac65..86eeec35 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/importlocal/LocalContactImportFragment.kt @@ -152,7 +152,7 @@ class LocalContactImportFragment(private val account: Account, private val uid: var localContact: LocalContact? = if (contact.uid == null) null else - addressBook.findByFilename(contact.uid!!) as LocalContact? + addressBook.findByUid(contact.uid!!) as LocalContact? if (localContact != null) { localContact.updateAsDirty(contact) @@ -183,7 +183,7 @@ class LocalContactImportFragment(private val account: Account, private val uid: var localGroup: LocalGroup? = if (group.uid == null) null else - addressBook.findByFilename(group.uid!!) as LocalGroup? + addressBook.findByUid(group.uid!!) as LocalGroup? if (localGroup != null) { localGroup.updateAsDirty(group, members) From 20a2c1b445e4df6adeadd58e13e48b171f41b6ed Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sun, 6 Sep 2020 10:54:09 +0300 Subject: [PATCH 57/83] Contacts sync: try setting the sync to expedited so it maybe happens sooner. --- .../syncadapter/syncadapter/AddressBooksSyncAdapterService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt index db7bc054..b90fdf42 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.kt @@ -64,6 +64,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { val syncExtras = Bundle(extras) syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true) syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true) + syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue) ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras) } From b637f25f4a7e2627de2201331cad82802f17ef75 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 12:28:13 +0300 Subject: [PATCH 58/83] Update etebase dep and adjust code accordingly --- app/build.gradle | 2 +- .../com/etesync/syncadapter/resource/LocalAddressBook.kt | 5 +++-- .../com/etesync/syncadapter/resource/LocalCalendar.kt | 3 ++- .../com/etesync/syncadapter/syncadapter/SyncManager.kt | 4 ++-- .../java/com/etesync/syncadapter/ui/AccountActivity.kt | 5 +++-- .../syncadapter/ui/etebase/CollectionMembersFragment.kt | 5 +++-- .../ui/etebase/CollectionMembersListFragment.kt | 5 +++-- .../syncadapter/ui/etebase/ViewCollectionFragment.kt | 9 +++++---- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8c0bddaf..a67ee7ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -134,7 +134,7 @@ dependencies { implementation "org.jetbrains.anko:anko-commons:0.10.4" implementation "com.etesync:journalmanager:1.1.1" - def etebaseVersion = '0.1.3-SNAPSHOT' + def etebaseVersion = '0.1.4-SNAPSHOT' implementation "com.etebase:client:$etebaseVersion" def acraVersion = '5.3.0' diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index a3aac76a..2aa795c2 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -19,6 +19,7 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import at.bitfire.vcard4android.* +import com.etebase.client.CollectionAccessLevel import com.etesync.syncadapter.App import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.log.Logger @@ -98,7 +99,7 @@ class LocalAddressBook( values.put(ContactsContract.Settings.SHOULD_SYNC, 1) values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) addressBook.settings = values - addressBook.readOnly = col.accessLevel == "ro" + addressBook.readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly return addressBook } @@ -250,7 +251,7 @@ class LocalAddressBook( account = future.result } - readOnly = col.accessLevel == "ro" + readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly Logger.log.info("Address book write permission? = ${!readOnly}") // make sure it will still be synchronized when contacts are updated diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 92142847..8017e033 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -18,6 +18,7 @@ import android.os.RemoteException import android.provider.CalendarContract import android.provider.CalendarContract.* import at.bitfire.ical4android.* +import com.etebase.client.CollectionAccessLevel import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.JournalEntity @@ -128,7 +129,7 @@ class LocalCalendar private constructor( if (withColor) values.put(Calendars.CALENDAR_COLOR, parseColor(meta.color)) - if (col.accessLevel == "ro") + if (col.accessLevel == CollectionAccessLevel.ReadOnly) values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) else { values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 6dcca0e9..1208b817 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -756,7 +756,7 @@ constructor(protected val context: Context, protected val account: Account, prot val localList = localCollection!!.findDeleted() val ret = ArrayList(localList.size) - val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro")) + val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly)) if (readOnly) { for (local in localList) { Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}") @@ -789,7 +789,7 @@ constructor(protected val context: Context, protected val account: Account, prot @Throws(CalendarStorageException::class, ContactsStorageException::class) protected open fun prepareDirty() { - val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro")) + val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly)) if (readOnly) { for (local in localDirty) { Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}") diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 55659693..514de400 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -27,6 +27,7 @@ import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS import at.bitfire.vcard4android.ContactsStorageException +import com.etebase.client.CollectionAccessLevel import com.etebase.client.CollectionManager import com.etebase.client.Utils import com.etebase.client.exceptions.EtebaseException @@ -394,8 +395,8 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe } val accessLevel = it.col.accessLevel - val isReadOnly = accessLevel == "ro" - val isAdmin = accessLevel == "adm" + val isReadOnly = accessLevel == CollectionAccessLevel.ReadOnly + val isAdmin = accessLevel == CollectionAccessLevel.Admin val metaColor = meta.color val color = if (!metaColor.isNullOrBlank()) LocalCalendar.parseColor(metaColor) else null diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt index f50bece7..6048a60c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersFragment.kt @@ -13,6 +13,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.etebase.client.CollectionAccessLevel import com.etebase.client.Utils import com.etebase.client.exceptions.EtebaseException import com.etebase.client.exceptions.NotFoundException @@ -81,7 +82,7 @@ class CollectionMembersFragment : Fragment() { val username = view.findViewById(R.id.username).text.toString() val readOnly = view.findViewById(R.id.read_only).isChecked - val frag = AddMemberFragment(model.value!!, collectionModel.value!!, username, if (readOnly) "ro" else "rw") + val frag = AddMemberFragment(model.value!!, collectionModel.value!!, username, if (readOnly) CollectionAccessLevel.ReadOnly else CollectionAccessLevel.ReadWrite) frag.show(childFragmentManager, null) } .setNegativeButton(android.R.string.no) { _, _ -> } @@ -90,7 +91,7 @@ class CollectionMembersFragment : Fragment() { } } -class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: String) : DialogFragment() { +class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: CollectionAccessLevel) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val progress = ProgressDialog(context) progress.setTitle(R.string.collection_members_adding) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt index df159325..b87d9dac 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionMembersListFragment.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.observe +import com.etebase.client.CollectionAccessLevel import com.etebase.client.CollectionMember import com.etebase.client.FetchOptions import com.etesync.syncadapter.CachedCollection @@ -78,7 +79,7 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val member = listAdapter?.getItem(position) as CollectionMember - if (member.accessLevel == "adm") { + if (member.accessLevel == CollectionAccessLevel.Admin) { AlertDialog.Builder(requireActivity()) .setIcon(R.drawable.ic_error_dark) .setTitle(R.string.collection_members_remove_title) @@ -111,7 +112,7 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis // FIXME: Also mark admins val readOnly = v.findViewById(R.id.read_only) - readOnly.visibility = if (member.accessLevel == "ro") View.VISIBLE else View.GONE + readOnly.visibility = if (member.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE return v } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt index 89cdcf20..280e3043 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.commit +import com.etebase.client.CollectionAccessLevel import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R @@ -63,7 +64,7 @@ class ViewCollectionFragment : Fragment() { val col = cachedCollection.col val meta = cachedCollection.meta - val isAdmin = col.accessLevel == "adm" + val isAdmin = col.accessLevel == CollectionAccessLevel.Admin val colorSquare = container.findViewById(R.id.color) val color = LocalCalendar.parseColor(meta.color) @@ -111,7 +112,7 @@ class ViewCollectionFragment : Fragment() { when (item.itemId) { R.id.on_edit -> { - if (cachedCollection.col.accessLevel == "adm") { + if (cachedCollection.col.accessLevel == CollectionAccessLevel.Admin) { parentFragmentManager.commit { replace(R.id.fragment_container, EditCollectionFragment(cachedCollection)) addToBackStack(EditCollectionFragment::class.java.name) @@ -126,7 +127,7 @@ class ViewCollectionFragment : Fragment() { } } R.id.on_manage_members -> { - if (cachedCollection.col.accessLevel == "adm") { + if (cachedCollection.col.accessLevel == CollectionAccessLevel.Admin) { parentFragmentManager.commit { replace(R.id.fragment_container, CollectionMembersFragment()) addToBackStack(null) @@ -140,7 +141,7 @@ class ViewCollectionFragment : Fragment() { dialog.show() } } R.id.on_import -> { - if (cachedCollection.col.accessLevel != "ro") { + if (cachedCollection.col.accessLevel != CollectionAccessLevel.ReadOnly) { parentFragmentManager.commit { replace(R.id.fragment_container, ImportCollectionFragment()) addToBackStack(null) From 048acdf26c772ce0e4230f9d3dfa8cb9169e51f1 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 18:26:18 +0300 Subject: [PATCH 59/83] Have a collection fetching cache like we had for etesync v1. This is there because we usually sync all of the adapters in tandem and we were fetching all of the collections multiple times because of it. --- .../syncadapter/syncadapter/SyncAdapterService.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt index 897b3171..743deb75 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.kt @@ -30,7 +30,6 @@ import com.etesync.syncadapter.* import com.etesync.journalmanager.Crypto import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.JournalManager -import com.etesync.syncadapter.* import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.JournalEntity @@ -247,6 +246,14 @@ abstract class SyncAdapterService : Service() { val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) synchronized(etebaseLocalCache) { + val cacheAge = 5 * 1000 // 5 seconds - it's just a hack for burst fetching + val now = System.currentTimeMillis() + val lastCollectionsFetch = collectionLastFetchMap[account.name] ?: 0 + + if (abs(now - lastCollectionsFetch) <= cacheAge) { + return@synchronized + } + val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) val colMgr = etebase.collectionManager var stoken = etebaseLocalCache.loadStoken() @@ -265,6 +272,7 @@ abstract class SyncAdapterService : Service() { done = colList.isDone etebaseLocalCache.saveStoken(stoken!!) } + collectionLastFetchMap[account.name] = now } httpClient.close() @@ -308,5 +316,6 @@ abstract class SyncAdapterService : Service() { companion object { val journalFetcher = CachedJournalFetcher() + var collectionLastFetchMap = HashMap() } } From bf050aa53b3cf5ef7d245e9b3d9d4323ccf81726 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 18:43:06 +0300 Subject: [PATCH 60/83] HttpClient: use the http client builder when using etesync If we don't use the http client builder we don't get cert4droid hooked which means we won't correctly support self-signed certificates. --- app/src/main/java/com/etesync/syncadapter/HttpClient.kt | 1 - .../etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt | 2 +- .../com/etesync/syncadapter/ui/etebase/CollectionActivity.kt | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/HttpClient.kt b/app/src/main/java/com/etesync/syncadapter/HttpClient.kt index c663d3f2..c3574f66 100644 --- a/app/src/main/java/com/etesync/syncadapter/HttpClient.kt +++ b/app/src/main/java/com/etesync/syncadapter/HttpClient.kt @@ -68,7 +68,6 @@ class HttpClient private constructor( ) { private var certManager: CustomCertManager? = null private var certificateAlias: String? = null - private var cache: Cache? = null private val orig = sharedClient.newBuilder() diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt index 5b14cde2..86fa3fc5 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/ChangeEncryptionPasswordActivity.kt @@ -69,7 +69,7 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() { } doAsync { - val httpClient = HttpClient.sharedClient + val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity).setForeground(true).build().okHttpClient try { Logger.log.info("Loging in with old password") diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt index 04da9dcd..f3dbf5f7 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -90,7 +90,8 @@ class AccountViewModel : ViewModel() { doAsync { val settings = AccountSettings(context, account) val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name) - val etebase = EtebaseLocalCache.getEtebase(context, HttpClient.sharedClient, settings) + val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient + val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings) val colMgr = etebase.collectionManager uiThread { holder.value = AccountHolder( From cec32851cf1db6ba2d872d2a00e4194104cd604a Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 19:13:50 +0300 Subject: [PATCH 61/83] Fix login/change password forms to have a consistent design. --- .../ui/setup/LoginCredentialsFragment.kt | 9 +++--- .../res/layout/change_encryption_password.xml | 1 + .../res/layout/login_credentials_fragment.xml | 30 ++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt index 846b6f9d..c22cfa1a 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment import com.etesync.syncadapter.Constants import com.etesync.syncadapter.R import com.etesync.syncadapter.ui.WebViewActivity +import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import net.cachapa.expandablelayout.ExpandableLayout import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -36,10 +37,10 @@ class LoginCredentialsFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val v = inflater.inflate(R.layout.login_credentials_fragment, container, false) - editUserName = v.findViewById(R.id.user_name) as EditText - editUrlPassword = v.findViewById(R.id.url_password) as TextInputLayout - showAdvanced = v.findViewById(R.id.show_advanced) as CheckedTextView - customServer = v.findViewById(R.id.custom_server) as EditText + editUserName = v.findViewById(R.id.user_name) + editUrlPassword = v.findViewById(R.id.url_password) + showAdvanced = v.findViewById(R.id.show_advanced) + customServer = v.findViewById(R.id.custom_server) if (savedInstanceState == null) { val activity = activity diff --git a/app/src/main/res/layout/change_encryption_password.xml b/app/src/main/res/layout/change_encryption_password.xml index ef4e6183..d1ea0eff 100644 --- a/app/src/main/res/layout/change_encryption_password.xml +++ b/app/src/main/res/layout/change_encryption_password.xml @@ -40,6 +40,7 @@ android:id="@+id/encryption_password" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="14dp" app:passwordToggleEnabled="true"> - + android:layout_marginBottom="14dp"> + + + - + android:layout_height="wrap_content"> + + From 5bf69d27d333559f20132027f15977d036eb8430 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 7 Sep 2020 19:23:08 +0300 Subject: [PATCH 62/83] Login page: remove EteSync-specific string + update forgot password link. --- app/src/main/java/com/etesync/syncadapter/Constants.java | 2 +- app/src/main/res/layout/login_credentials_fragment.xml | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/Constants.java b/app/src/main/java/com/etesync/syncadapter/Constants.java index 8dc1e370..9c008ab0 100644 --- a/app/src/main/java/com/etesync/syncadapter/Constants.java +++ b/app/src/main/java/com/etesync/syncadapter/Constants.java @@ -32,7 +32,7 @@ public class Constants { public static final Uri dashboard = webUri.buildUpon().appendEncodedPath("dashboard/").build(); public static final Uri faqUri = webUri.buildUpon().appendEncodedPath("faq/").build(); public static final Uri helpUri = webUri.buildUpon().appendEncodedPath("user-guide/android/").build(); - public static final Uri forgotPassword = webUri.buildUpon().appendEncodedPath("accounts/password/reset/").build(); + public static final Uri forgotPassword = faqUri.buildUpon().fragment("forgot-password").build(); public static final Uri serviceUrl = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://api.etesync.com/" : DEBUG_REMOTE_URL); public static final String etebaseServiceUrl = (DEBUG_REMOTE_URL == null) ? "https://api.etebase.com/partner/etesync/" : DEBUG_REMOTE_URL; diff --git a/app/src/main/res/layout/login_credentials_fragment.xml b/app/src/main/res/layout/login_credentials_fragment.xml index 09d0efa9..5cd89950 100644 --- a/app/src/main/res/layout/login_credentials_fragment.xml +++ b/app/src/main/res/layout/login_credentials_fragment.xml @@ -29,12 +29,6 @@ android:text="@string/login_enter_service_details" android:layout_marginBottom="14dp"/> - - Date: Tue, 8 Sep 2020 10:05:25 +0300 Subject: [PATCH 63/83] Strings: add a username string. --- app/src/main/res/layout/login_credentials_fragment.xml | 2 +- app/src/main/res/values/strings.xml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/login_credentials_fragment.xml b/app/src/main/res/layout/login_credentials_fragment.xml index 5cd89950..7448d57b 100644 --- a/app/src/main/res/layout/login_credentials_fragment.xml +++ b/app/src/main/res/layout/login_credentials_fragment.xml @@ -37,7 +37,7 @@ android:id="@+id/user_name" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/login_email_address" + android:hint="@string/login_username" android:autofillHints="emailAddress" android:inputType="textEmailAddress"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49d5e0b9..74b65d46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,8 +210,9 @@ Add account - Username - Valid username required + Username + Email + Valid email required Password EteSync Server URL Invalid URL found, did you forget to include https://? From 44503715a8f9301388b61c482ffe119305f973aa Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 8 Sep 2020 10:38:52 +0300 Subject: [PATCH 64/83] Login fragment: clear errors on validation success --- .../etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt index c22cfa1a..775dbee4 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/LoginCredentialsFragment.kt @@ -93,12 +93,16 @@ class LoginCredentialsFragment : Fragment() { 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 @@ -109,6 +113,7 @@ class LoginCredentialsFragment : Fragment() { 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 From 2eeee1214f5c358841cc7c49c711d3216a00e0b4 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 8 Sep 2020 09:44:14 +0300 Subject: [PATCH 65/83] Signup: add a signup fragment so people can sign up from the app --- .../syncadapter/ui/etebase/SignupFragment.kt | 226 ++++++++++++++++++ .../syncadapter/ui/setup/LoginActivity.kt | 14 +- .../ui/setup/LoginCredentialsFragment.kt | 22 +- app/src/main/res/layout/signup_fragment.xml | 134 +++++++++++ app/src/main/res/values/strings.xml | 6 +- 5 files changed, 375 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/SignupFragment.kt create mode 100644 app/src/main/res/layout/signup_fragment.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/SignupFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/SignupFragment.kt new file mode 100644 index 00000000..2250849b --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/SignupFragment.kt @@ -0,0 +1,226 @@ +/* + * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.ui.etebase + +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckedTextView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.fragment.app.viewModels +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.observe +import com.etebase.client.Account +import com.etebase.client.Client +import com.etebase.client.User +import com.etebase.client.exceptions.EtebaseException +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.HttpClient +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder +import com.etesync.syncadapter.ui.setup.CreateAccountFragment +import com.etesync.syncadapter.ui.setup.DetectConfigurationFragment +import com.etesync.syncadapter.ui.setup.LoginCredentialsFragment +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 org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.net.URI +import java.util.concurrent.Future + +class SignupFragment(private val initialUsername: String?, private val initialPassword: String?) : 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 + + + 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) + + if (savedInstanceState == null) { + editUserName.editText?.setText(initialUsername ?: "") + editPassword.editText?.setText(initialPassword ?: "") + } + + val login = v.findViewById