From 5da8edd54da7fa0de638949db34bd8dfcc477924 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 25 Aug 2020 15:00:27 +0300 Subject: [PATCH] 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")