Convert account settings activities to Kotlin

pull/1051/head
Alex Baker 5 years ago
parent eb38972ac2
commit 919ba42098

@ -11,6 +11,7 @@ import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
@ -106,7 +107,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test task deletion */ /** Test task deletion */
@Test @Test
fun testTDeletion() { fun testTDeletion() = runBlocking {
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
// create task "happy" // create task "happy"
@ -133,7 +134,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test passing invalid task indices to various things */ /** Test passing invalid task indices to various things */
@Test @Test
fun testInvalidIndex() { fun testInvalidIndex() = runBlocking {
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
assertNull(taskDao.fetchBlocking(1)) assertNull(taskDao.fetchBlocking(1))
taskDeleter.delete(listOf(1L)) taskDeleter.delete(listOf(1L))

@ -1,389 +0,0 @@
package org.tasks.caldav;
import static com.todoroo.astrid.data.Task.NO_ID;
import static org.tasks.Strings.isNullOrEmpty;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.util.Linkify;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import at.bitfire.dav4jvm.exception.HttpException;
import butterknife.ButterKnife;
import butterknife.OnFocusChange;
import butterknife.OnTextChanged;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import com.todoroo.astrid.service.TaskDeleter;
import java.net.ConnectException;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseActivity;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavDaoBlocking;
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.security.KeyStoreEncryption;
import org.tasks.ui.DisplayableException;
import timber.log.Timber;
public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingAppCompatActivity
implements Toolbar.OnMenuItemClickListener {
public static final String EXTRA_CALDAV_DATA = "caldavData"; // $NON-NLS-1$
protected static final String PASSWORD_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
@Inject protected CaldavDaoBlocking caldavDao;
@Inject protected KeyStoreEncryption encryption;
@Inject DialogBuilder dialogBuilder;
@Inject TaskDeleter taskDeleter;
@Inject Inventory inventory;
protected CaldavAccount caldavAccount;
protected ActivityCaldavAccountSettingsBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityCaldavAccountSettingsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ButterKnife.bind(this);
caldavAccount =
savedInstanceState == null
? getIntent().getParcelableExtra(EXTRA_CALDAV_DATA)
: savedInstanceState.getParcelable(EXTRA_CALDAV_DATA);
if (caldavAccount == null || caldavAccount.getId() == NO_ID) {
binding.nameLayout.setVisibility(View.GONE);
binding.description.setVisibility(View.VISIBLE);
binding.description.setText(getDescription());
Linkify.addLinks(binding.description, Linkify.WEB_URLS);
} else {
binding.nameLayout.setVisibility(View.VISIBLE);
binding.description.setVisibility(View.GONE);
}
if (savedInstanceState == null) {
if (caldavAccount != null) {
binding.name.setText(caldavAccount.getName());
binding.url.setText(caldavAccount.getUrl());
binding.user.setText(caldavAccount.getUsername());
if (!isNullOrEmpty(caldavAccount.getPassword())) {
binding.password.setText(PASSWORD_MASK);
}
binding.repeat.setChecked(caldavAccount.isSuppressRepeatingTasks());
}
}
Toolbar toolbar = binding.toolbar.toolbar;
toolbar.setTitle(
caldavAccount == null ? getString(R.string.add_account) : caldavAccount.getName());
toolbar.setNavigationIcon(getDrawable(R.drawable.ic_outline_save_24px));
toolbar.setNavigationOnClickListener(v -> save());
toolbar.inflateMenu(R.menu.menu_caldav_account_settings);
toolbar.setOnMenuItemClickListener(this);
toolbar.showOverflowMenu();
themeColor.apply(toolbar);
if (caldavAccount == null) {
toolbar.getMenu().findItem(R.id.remove).setVisible(false);
binding.name.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT);
}
if (!inventory.hasPro()) {
newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(
R.string.button_subscribe,
v -> startActivity(new Intent(this, PurchaseActivity.class)))
.show();
}
}
protected abstract @StringRes int getDescription();
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(EXTRA_CALDAV_DATA, caldavAccount);
}
private void showProgressIndicator() {
binding.progressBar.progressBar.setVisibility(View.VISIBLE);
}
protected void hideProgressIndicator() {
binding.progressBar.progressBar.setVisibility(View.GONE);
}
private boolean requestInProgress() {
return binding.progressBar.progressBar.getVisibility() == View.VISIBLE;
}
@OnTextChanged(R.id.name)
void onNameChanged() {
binding.nameLayout.setError(null);
}
@OnTextChanged(R.id.url)
void onUrlChanged() {
binding.urlLayout.setError(null);
}
@OnTextChanged(R.id.user)
void onUserChanged() {
binding.userLayout.setError(null);
}
@OnTextChanged(R.id.password)
void onPasswordChanged() {
binding.passwordLayout.setError(null);
}
@OnFocusChange(R.id.password)
void onPasswordFocused(boolean hasFocus) {
if (hasFocus) {
if (PASSWORD_MASK.equals(binding.password.getText().toString())) {
binding.password.setText("");
}
} else {
if (TextUtils.isEmpty(binding.password.getText()) && caldavAccount != null) {
binding.password.setText(PASSWORD_MASK);
}
}
}
protected String getNewName() {
String name = binding.name.getText().toString().trim();
return isNullOrEmpty(name) ? getNewUsername() : name;
}
protected String getNewURL() {
return binding.url.getText().toString().trim();
}
protected String getNewUsername() {
return binding.user.getText().toString().trim();
}
boolean passwordChanged() {
return caldavAccount == null || !PASSWORD_MASK.equals(binding.password.getText().toString().trim());
}
protected abstract String getNewPassword();
private void save() {
if (requestInProgress()) {
return;
}
String username = getNewUsername();
String url = getNewURL();
String password = getNewPassword();
boolean failed = false;
if (isNullOrEmpty(url)) {
binding.urlLayout.setError(getString(R.string.url_required));
failed = true;
} else {
Uri baseURL = Uri.parse(url);
String scheme = baseURL.getScheme();
if ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
String host = baseURL.getHost();
if (isNullOrEmpty(host)) {
binding.urlLayout.setError(getString(R.string.url_host_name_required));
failed = true;
} else {
try {
host = IDN.toASCII(host);
} catch (Exception e) {
Timber.e(e);
}
String path = baseURL.getEncodedPath();
int port = baseURL.getPort();
try {
new URI(scheme, null, host, port, path, null, null);
} catch (URISyntaxException e) {
binding.urlLayout.setError(e.getLocalizedMessage());
failed = true;
}
}
} else {
binding.urlLayout.setError(getString(R.string.url_invalid_scheme));
failed = true;
}
}
if (isNullOrEmpty(username)) {
binding.userLayout.setError(getString(R.string.username_required));
failed = true;
}
if (isNullOrEmpty(password)) {
binding.passwordLayout.setError(getString(R.string.password_required));
failed = true;
}
if (failed) {
return;
}
if (caldavAccount == null) {
showProgressIndicator();
addAccount(url, username, password);
} else if (needsValidation()) {
showProgressIndicator();
updateAccount(url, username, password);
} else if (hasChanges()) {
updateAccount();
} else {
finish();
}
}
protected abstract void addAccount(String url, String username, String password);
protected abstract void updateAccount(String url, String username, String password);
protected abstract void updateAccount();
protected abstract String getHelpUrl();
protected void requestFailed(Throwable t) {
hideProgressIndicator();
if (t instanceof HttpException) {
if (((HttpException) t).getCode() == 401) {
showSnackbar(R.string.invalid_username_or_password);
} else {
showSnackbar(t.getMessage());
}
} else if (t instanceof DisplayableException) {
showSnackbar(((DisplayableException) t).getResId());
} else if (t instanceof ConnectException) {
showSnackbar(R.string.network_error);
} else {
Timber.e(t);
showSnackbar(R.string.error_adding_account, t.getMessage());
}
}
private void showSnackbar(int resId, Object... formatArgs) {
showSnackbar(getString(resId, formatArgs));
}
private void showSnackbar(String message) {
newSnackbar(message).show();
}
private Snackbar newSnackbar(String message) {
Snackbar snackbar =
Snackbar.make(binding.rootLayout, message, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setActionTextColor(getColor(R.color.snackbar_action_color));
snackbar
.getView()
.setBackgroundColor(getColor(R.color.snackbar_background));
return snackbar;
}
private boolean hasChanges() {
if (caldavAccount == null) {
return !isNullOrEmpty(binding.name.getText().toString().trim())
|| !isNullOrEmpty(getNewPassword())
|| !isNullOrEmpty(binding.url.getText().toString().trim())
|| !isNullOrEmpty(getNewUsername())
|| binding.repeat.isChecked();
}
return needsValidation()
|| !getNewName().equals(caldavAccount.getName())
|| binding.repeat.isChecked() != caldavAccount.isSuppressRepeatingTasks();
}
protected boolean needsValidation() {
return !getNewURL().equals(caldavAccount.getUrl())
|| !getNewUsername().equals(caldavAccount.getUsername())
|| passwordChanged();
}
@Override
public void finish() {
if (!requestInProgress()) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(binding.name.getWindowToken(), 0);
super.finish();
}
}
@Override
public void onBackPressed() {
discard();
}
private void removeAccountPrompt() {
if (requestInProgress()) {
return;
}
dialogBuilder
.newDialog()
.setMessage(R.string.logout_warning, caldavAccount.getName())
.setPositiveButton(R.string.remove, (dialog, which) -> removeAccount())
.setNegativeButton(android.R.string.cancel, null)
.show();
}
protected void removeAccount() {
taskDeleter.delete(caldavAccount);
setResult(RESULT_OK);
finish();
}
private void discard() {
if (requestInProgress()) {
return;
}
if (!hasChanges()) {
finish();
} else {
dialogBuilder
.newDialog(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which) -> finish())
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_help:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getHelpUrl())));
break;
case R.id.remove:
removeAccountPrompt();
break;
}
return onOptionsItemSelected(item);
}
}

@ -0,0 +1,353 @@
package org.tasks.caldav
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.text.util.Linkify
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import at.bitfire.dav4jvm.exception.HttpException
import butterknife.ButterKnife
import butterknife.OnFocusChange
import butterknife.OnTextChanged
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDaoBlocking
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.ThemedInjectingAppCompatActivity
import org.tasks.security.KeyStoreEncryption
import org.tasks.ui.DisplayableException
import timber.log.Timber
import java.net.ConnectException
import java.net.IDN
import java.net.URI
import java.net.URISyntaxException
import javax.inject.Inject
abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var caldavDao: CaldavDaoBlocking
@Inject lateinit var encryption: KeyStoreEncryption
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var inventory: Inventory
protected var caldavAccount: CaldavAccount? = null
protected var binding: ActivityCaldavAccountSettingsBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCaldavAccountSettingsBinding.inflate(layoutInflater)
setContentView(binding!!.root)
ButterKnife.bind(this)
caldavAccount = if (savedInstanceState == null) intent.getParcelableExtra(EXTRA_CALDAV_DATA) else savedInstanceState.getParcelable(EXTRA_CALDAV_DATA)
if (caldavAccount == null || caldavAccount!!.id == Task.NO_ID) {
binding!!.nameLayout.visibility = View.GONE
binding!!.description.visibility = View.VISIBLE
binding!!.description.setText(description)
Linkify.addLinks(binding!!.description, Linkify.WEB_URLS)
} else {
binding!!.nameLayout.visibility = View.VISIBLE
binding!!.description.visibility = View.GONE
}
if (savedInstanceState == null) {
if (caldavAccount != null) {
binding!!.name.setText(caldavAccount!!.name)
binding!!.url.setText(caldavAccount!!.url)
binding!!.user.setText(caldavAccount!!.username)
if (!isNullOrEmpty(caldavAccount!!.password)) {
binding!!.password.setText(PASSWORD_MASK)
}
binding!!.repeat.isChecked = caldavAccount!!.isSuppressRepeatingTasks
}
}
val toolbar = binding!!.toolbar.toolbar
toolbar.title = if (caldavAccount == null) getString(R.string.add_account) else caldavAccount!!.name
toolbar.navigationIcon = getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener { save() }
toolbar.inflateMenu(R.menu.menu_caldav_account_settings)
toolbar.setOnMenuItemClickListener(this)
toolbar.showOverflowMenu()
themeColor.apply(toolbar)
if (caldavAccount == null) {
toolbar.menu.findItem(R.id.remove).isVisible = false
binding!!.name.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding!!.name, InputMethodManager.SHOW_IMPLICIT)
}
if (!inventory.hasPro()) {
newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) {
startActivity(Intent(this, PurchaseActivity::class.java))
}
.show()
}
}
@get:StringRes
protected abstract val description: Int
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_CALDAV_DATA, caldavAccount)
}
private fun showProgressIndicator() {
binding!!.progressBar.progressBar.visibility = View.VISIBLE
}
protected fun hideProgressIndicator() {
binding!!.progressBar.progressBar.visibility = View.GONE
}
private fun requestInProgress(): Boolean {
return binding!!.progressBar.progressBar.visibility == View.VISIBLE
}
@OnTextChanged(R.id.name)
fun onNameChanged() {
binding!!.nameLayout.error = null
}
@OnTextChanged(R.id.url)
fun onUrlChanged() {
binding!!.urlLayout.error = null
}
@OnTextChanged(R.id.user)
fun onUserChanged() {
binding!!.userLayout.error = null
}
@OnTextChanged(R.id.password)
fun onPasswordChanged() {
binding!!.passwordLayout.error = null
}
@OnFocusChange(R.id.password)
fun onPasswordFocused(hasFocus: Boolean) {
if (hasFocus) {
if (PASSWORD_MASK == binding!!.password.text.toString()) {
binding!!.password.setText("")
}
} else {
if (TextUtils.isEmpty(binding!!.password.text) && caldavAccount != null) {
binding!!.password.setText(PASSWORD_MASK)
}
}
}
protected val newName: String
get() {
val name = binding!!.name.text.toString().trim { it <= ' ' }
return if (isNullOrEmpty(name)) newUsername else name
}
protected open val newURL: String
get() = binding!!.url.text.toString().trim { it <= ' ' }
protected val newUsername: String
get() = binding!!.user.text.toString().trim { it <= ' ' }
fun passwordChanged(): Boolean {
return caldavAccount == null || PASSWORD_MASK != binding!!.password.text.toString().trim { it <= ' ' }
}
protected abstract val newPassword: String?
private fun save() {
if (requestInProgress()) {
return
}
val username = newUsername
val url = newURL
val password = newPassword
var failed = false
if (isNullOrEmpty(url)) {
binding!!.urlLayout.error = getString(R.string.url_required)
failed = true
} else {
val baseURL = Uri.parse(url)
val scheme = baseURL.scheme
if ("https".equals(scheme, ignoreCase = true) || "http".equals(scheme, ignoreCase = true)) {
var host = baseURL.host
if (isNullOrEmpty(host)) {
binding!!.urlLayout.error = getString(R.string.url_host_name_required)
failed = true
} else {
try {
host = IDN.toASCII(host)
} catch (e: Exception) {
Timber.e(e)
}
val path = baseURL.encodedPath
val port = baseURL.port
try {
URI(scheme, null, host, port, path, null, null)
} catch (e: URISyntaxException) {
binding!!.urlLayout.error = e.localizedMessage
failed = true
}
}
} else {
binding!!.urlLayout.error = getString(R.string.url_invalid_scheme)
failed = true
}
}
if (isNullOrEmpty(username)) {
binding!!.userLayout.error = getString(R.string.username_required)
failed = true
}
if (isNullOrEmpty(password)) {
binding!!.passwordLayout.error = getString(R.string.password_required)
failed = true
}
when {
failed -> return
caldavAccount == null -> {
showProgressIndicator()
addAccount(url, username, password)
}
needsValidation() -> {
showProgressIndicator()
updateAccount(url, username, password)
}
hasChanges() -> {
updateAccount()
}
else -> {
finish()
}
}
}
protected abstract fun addAccount(url: String?, username: String?, password: String?)
protected abstract fun updateAccount(url: String?, username: String?, password: String?)
protected abstract fun updateAccount()
protected abstract val helpUrl: String?
protected fun requestFailed(t: Throwable) {
hideProgressIndicator()
if (t is HttpException) {
if (t.code == 401) {
showSnackbar(R.string.invalid_username_or_password)
} else {
showSnackbar(t.message)
}
} else if (t is DisplayableException) {
showSnackbar(t.resId)
} else if (t is ConnectException) {
showSnackbar(R.string.network_error)
} else {
Timber.e(t)
showSnackbar(R.string.error_adding_account, t.message!!)
}
}
private fun showSnackbar(resId: Int, vararg formatArgs: Any) {
showSnackbar(getString(resId, *formatArgs))
}
private fun showSnackbar(message: String?) {
newSnackbar(message).show()
}
private fun newSnackbar(message: String?): Snackbar {
val snackbar = Snackbar.make(binding!!.rootLayout, message!!, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar
.view
.setBackgroundColor(getColor(R.color.snackbar_background))
return snackbar
}
private fun hasChanges(): Boolean {
return if (caldavAccount == null) {
(!isNullOrEmpty(binding!!.name.text.toString().trim { it <= ' ' })
|| !isNullOrEmpty(newPassword)
|| !isNullOrEmpty(binding!!.url.text.toString().trim { it <= ' ' })
|| !isNullOrEmpty(newUsername)
|| binding!!.repeat.isChecked)
} else needsValidation()
|| newName != caldavAccount!!.name
|| binding!!.repeat.isChecked != caldavAccount!!.isSuppressRepeatingTasks
}
protected open fun needsValidation(): Boolean {
return (newURL != caldavAccount!!.url
|| newUsername != caldavAccount!!.username
|| passwordChanged())
}
override fun finish() {
if (!requestInProgress()) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(binding!!.name.windowToken, 0)
super.finish()
}
}
override fun onBackPressed() {
discard()
}
private fun removeAccountPrompt() {
if (requestInProgress()) {
return
}
dialogBuilder
.newDialog()
.setMessage(R.string.logout_warning, caldavAccount!!.name)
.setPositiveButton(R.string.remove) { _, _ -> removeAccount() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
protected open fun removeAccount() {
taskDeleter.delete(caldavAccount!!)
setResult(Activity.RESULT_OK)
finish()
}
private fun discard() {
if (requestInProgress()) {
return
}
if (!hasChanges()) {
finish()
} else {
dialogBuilder
.newDialog(R.string.discard_changes)
.setPositiveButton(R.string.discard) { _, _ -> finish() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_help -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(helpUrl)))
R.id.remove -> removeAccountPrompt()
}
return onOptionsItemSelected(item)
}
companion object {
const val EXTRA_CALDAV_DATA = "caldavData" // $NON-NLS-1$
const val PASSWORD_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
}
}

@ -1,98 +0,0 @@
package org.tasks.caldav;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import com.todoroo.astrid.helper.UUIDHelper;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.CaldavAccount;
import timber.log.Timber;
@AndroidEntryPoint
public class CaldavAccountSettingsActivity extends BaseCaldavAccountSettingsActivity
implements Toolbar.OnMenuItemClickListener {
@Inject CaldavClient client;
private AddCaldavAccountViewModel addCaldavAccountViewModel;
private UpdateCaldavAccountViewModel updateCaldavAccountViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewModelProvider provider = new ViewModelProvider(this);
addCaldavAccountViewModel = provider.get(AddCaldavAccountViewModel.class);
updateCaldavAccountViewModel = provider.get(UpdateCaldavAccountViewModel.class);
addCaldavAccountViewModel.observe(this, this::addAccount, this::requestFailed);
updateCaldavAccountViewModel.observe(this, this::updateAccount, this::requestFailed);
}
@Override
protected int getDescription() {
return R.string.caldav_account_description;
}
private void addAccount(String principal) {
hideProgressIndicator();
Timber.d("Found principal: %s", principal);
CaldavAccount newAccount = new CaldavAccount();
newAccount.setName(getNewName());
newAccount.setUrl(principal);
newAccount.setUsername(getNewUsername());
newAccount.setPassword(encryption.encrypt(getNewPassword()));
newAccount.setUuid(UUIDHelper.newUUID());
newAccount.setId(caldavDao.insert(newAccount));
setResult(RESULT_OK);
finish();
}
private void updateAccount(String principal) {
hideProgressIndicator();
caldavAccount.setName(getNewName());
caldavAccount.setUrl(principal);
caldavAccount.setUsername(getNewUsername());
caldavAccount.setError("");
if (passwordChanged()) {
caldavAccount.setPassword(encryption.encrypt(getNewPassword()));
}
caldavAccount.setSuppressRepeatingTasks(binding.repeat.isChecked());
caldavDao.update(caldavAccount);
setResult(RESULT_OK);
finish();
}
@Override
protected void addAccount(String url, String username, String password) {
addCaldavAccountViewModel.addAccount(client, url, username, password);
}
@Override
protected void updateAccount(String url, String username, String password) {
updateCaldavAccountViewModel.updateCaldavAccount(client, url, username, password);
}
@Override
protected void updateAccount() {
updateAccount(caldavAccount.getUrl());
}
@Override
protected String getNewPassword() {
String input = binding.password.getText().toString().trim();
return PASSWORD_MASK.equals(input) ? encryption.decrypt(caldavAccount.getPassword()) : input;
}
@Override
protected String getHelpUrl() {
return "https://tasks.org/caldav";
}
}

@ -0,0 +1,83 @@
package org.tasks.caldav
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.data.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class CaldavAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var client: CaldavClient
private var addCaldavAccountViewModel: AddCaldavAccountViewModel? = null
private var updateCaldavAccountViewModel: UpdateCaldavAccountViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val provider = ViewModelProvider(this)
addCaldavAccountViewModel = provider.get(AddCaldavAccountViewModel::class.java)
updateCaldavAccountViewModel = provider.get(UpdateCaldavAccountViewModel::class.java)
addCaldavAccountViewModel!!.observe(this, Observer { principal: String -> this.addAccount(principal) }, Observer { t: Throwable? -> requestFailed(t!!) })
updateCaldavAccountViewModel!!.observe(this, Observer { principal: String? -> this.updateAccount(principal) }, Observer { t: Throwable? -> requestFailed(t!!) })
}
override val description: Int
get() = R.string.caldav_account_description
private fun addAccount(principal: String) {
hideProgressIndicator()
Timber.d("Found principal: %s", principal)
val newAccount = CaldavAccount()
newAccount.name = newName
newAccount.url = principal
newAccount.username = newUsername
newAccount.password = encryption.encrypt(newPassword!!)
newAccount.uuid = UUIDHelper.newUUID()
newAccount.id = caldavDao.insert(newAccount)
setResult(Activity.RESULT_OK)
finish()
}
private fun updateAccount(principal: String?) {
hideProgressIndicator()
caldavAccount!!.name = newName
caldavAccount!!.url = principal
caldavAccount!!.username = newUsername
caldavAccount!!.error = ""
if (passwordChanged()) {
caldavAccount!!.password = encryption.encrypt(newPassword!!)
}
caldavAccount!!.isSuppressRepeatingTasks = binding!!.repeat.isChecked
caldavDao.update(caldavAccount!!)
setResult(Activity.RESULT_OK)
finish()
}
override fun addAccount(url: String?, username: String?, password: String?) {
addCaldavAccountViewModel!!.addAccount(client, url, username, password)
}
override fun updateAccount(url: String?, username: String?, password: String?) {
updateCaldavAccountViewModel!!.updateCaldavAccount(client, url, username, password)
}
override fun updateAccount() {
updateAccount(caldavAccount!!.url)
}
override val newPassword: String?
get() {
val input = binding!!.password.text.toString().trim { it <= ' ' }
return if (PASSWORD_MASK == input) encryption.decrypt(caldavAccount!!.password) else input
}
override val helpUrl: String
get() = "https://tasks.org/caldav"
}

@ -1,207 +0,0 @@
package org.tasks.etesync;
import static com.todoroo.astrid.data.Task.NO_ID;
import static org.tasks.Strings.isNullOrEmpty;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.util.Pair;
import androidx.lifecycle.ViewModelProvider;
import butterknife.OnCheckedChanged;
import com.etesync.journalmanager.Crypto.CryptoManager;
import com.etesync.journalmanager.Exceptions.IntegrityException;
import com.etesync.journalmanager.Exceptions.VersionTooNewException;
import com.etesync.journalmanager.UserInfoManager.UserInfo;
import com.todoroo.astrid.helper.UUIDHelper;
import dagger.hilt.android.AndroidEntryPoint;
import io.reactivex.Completable;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.caldav.BaseCaldavAccountSettingsActivity;
import org.tasks.data.CaldavAccount;
import timber.log.Timber;
@AndroidEntryPoint
public class EteSyncAccountSettingsActivity extends BaseCaldavAccountSettingsActivity
implements Toolbar.OnMenuItemClickListener {
private static final int REQUEST_ENCRYPTION_PASSWORD = 10101;
@Inject EteSyncClient eteSyncClient;
private AddEteSyncAccountViewModel addAccountViewModel;
private UpdateEteSyncAccountViewModel updateAccountViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.repeat.setVisibility(View.GONE);
binding.showAdvanced.setVisibility(View.VISIBLE);
updateUrlVisibility();
ViewModelProvider provider = new ViewModelProvider(this);
addAccountViewModel = provider.get(AddEteSyncAccountViewModel.class);
updateAccountViewModel = provider.get(UpdateEteSyncAccountViewModel.class);
}
@Override
protected void onResume() {
super.onResume();
if (!isFinishing()) {
addAccountViewModel.observe(this, this::addAccount, this::requestFailed);
updateAccountViewModel.observe(this, this::updateAccount, this::requestFailed);
}
}
@Override
protected void onPause() {
super.onPause();
addAccountViewModel.removeObserver(this);
updateAccountViewModel.removeObserver(this);
}
@Override
protected int getDescription() {
return R.string.etesync_account_description;
}
private void addAccount(Pair<UserInfo, String> userInfoAndToken) {
caldavAccount = new CaldavAccount();
caldavAccount.setAccountType(CaldavAccount.TYPE_ETESYNC);
caldavAccount.setUuid(UUIDHelper.newUUID());
applyTo(caldavAccount, userInfoAndToken);
}
private void updateAccount(Pair<UserInfo, String> userInfoAndToken) {
caldavAccount.setError("");
applyTo(caldavAccount, userInfoAndToken);
}
private void applyTo(CaldavAccount account, Pair<UserInfo, String> userInfoAndToken) {
hideProgressIndicator();
account.setName(getNewName());
account.setUrl(getNewURL());
account.setUsername(getNewUsername());
String token = userInfoAndToken.second;
if (!token.equals(account.getPassword(encryption))) {
account.setPassword(encryption.encrypt(token));
}
UserInfo userInfo = userInfoAndToken.first;
if (testUserInfo(userInfo)) {
saveAccountAndFinish();
} else {
Intent intent = new Intent(this, EncryptionSettingsActivity.class);
intent.putExtra(EncryptionSettingsActivity.EXTRA_USER_INFO, userInfo);
intent.putExtra(EncryptionSettingsActivity.EXTRA_ACCOUNT, account);
startActivityForResult(intent, REQUEST_ENCRYPTION_PASSWORD);
}
}
private boolean testUserInfo(UserInfo userInfo) {
String encryptionKey = caldavAccount.getEncryptionPassword(encryption);
if (userInfo != null && !isNullOrEmpty(encryptionKey)) {
try {
CryptoManager cryptoManager =
new CryptoManager(userInfo.getVersion(), encryptionKey, "userInfo");
userInfo.verify(cryptoManager);
return true;
} catch (IntegrityException | VersionTooNewException e) {
Timber.e(e);
}
}
return false;
}
@OnCheckedChanged(R.id.show_advanced)
void toggleUrl() {
updateUrlVisibility();
}
private void updateUrlVisibility() {
binding.urlLayout.setVisibility(binding.showAdvanced.isChecked() ? View.VISIBLE : View.GONE);
}
@Override
protected boolean needsValidation() {
return super.needsValidation() || isNullOrEmpty(caldavAccount.getEncryptionKey());
}
@Override
protected void addAccount(String url, String username, String password) {
addAccountViewModel.addAccount(eteSyncClient, url, username, password);
}
@Override
protected void updateAccount(String url, String username, String password) {
updateAccountViewModel.updateAccount(
eteSyncClient,
url,
username,
PASSWORD_MASK.equals(password) ? null : password,
caldavAccount.getPassword(encryption));
}
@Override
protected void updateAccount() {
caldavAccount.setName(getNewName());
saveAccountAndFinish();
}
@Override
protected String getNewURL() {
String url = super.getNewURL();
return isNullOrEmpty(url) ? getString(R.string.etesync_url) : url;
}
@Override
protected String getNewPassword() {
return binding.password.getText().toString().trim();
}
@Override
protected String getHelpUrl() {
return "https://tasks.org/etesync";
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_ENCRYPTION_PASSWORD) {
if (resultCode == RESULT_OK) {
String key = data.getStringExtra(EncryptionSettingsActivity.EXTRA_DERIVED_KEY);
caldavAccount.setEncryptionKey(encryption.encrypt(key));
saveAccountAndFinish();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void saveAccountAndFinish() {
if (caldavAccount.getId() == NO_ID) {
caldavDao.insert(caldavAccount);
} else {
caldavDao.update(caldavAccount);
}
setResult(RESULT_OK);
finish();
}
@Override
protected void removeAccount() {
if (caldavAccount != null) {
Completable.fromAction(() -> eteSyncClient.forAccount(caldavAccount).invalidateToken())
.subscribeOn(Schedulers.io())
.subscribe();
}
super.removeAccount();
}
}

@ -0,0 +1,187 @@
package org.tasks.etesync
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.util.Pair
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import butterknife.OnCheckedChanged
import com.etesync.journalmanager.Crypto.CryptoManager
import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException
import com.etesync.journalmanager.UserInfoManager
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.Completable
import io.reactivex.schedulers.Schedulers
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var eteSyncClient: EteSyncClient
private var addAccountViewModel: AddEteSyncAccountViewModel? = null
private var updateAccountViewModel: UpdateEteSyncAccountViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding!!.repeat.visibility = View.GONE
binding!!.showAdvanced.visibility = View.VISIBLE
updateUrlVisibility()
val provider = ViewModelProvider(this)
addAccountViewModel = provider.get(AddEteSyncAccountViewModel::class.java)
updateAccountViewModel = provider.get(UpdateEteSyncAccountViewModel::class.java)
}
override fun onResume() {
super.onResume()
if (!isFinishing) {
addAccountViewModel!!.observe(this, Observer { userInfoAndToken: Pair<UserInfoManager.UserInfo, String> -> this.addAccount(userInfoAndToken) }, Observer { t: Throwable? -> requestFailed(t!!) })
updateAccountViewModel!!.observe(this, Observer { userInfoAndToken: Pair<UserInfoManager.UserInfo, String> -> this.updateAccount(userInfoAndToken) }, Observer { t: Throwable? -> requestFailed(t!!) })
}
}
override fun onPause() {
super.onPause()
addAccountViewModel!!.removeObserver(this)
updateAccountViewModel!!.removeObserver(this)
}
override val description: Int
get() = R.string.etesync_account_description
private fun addAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
caldavAccount = CaldavAccount()
caldavAccount!!.accountType = CaldavAccount.TYPE_ETESYNC
caldavAccount!!.uuid = UUIDHelper.newUUID()
applyTo(caldavAccount!!, userInfoAndToken)
}
private fun updateAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
caldavAccount!!.error = ""
applyTo(caldavAccount!!, userInfoAndToken)
}
private fun applyTo(account: CaldavAccount, userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
hideProgressIndicator()
account.name = newName
account.url = newURL
account.username = newUsername
val token = userInfoAndToken.second
if (token != account.getPassword(encryption)) {
account.password = encryption.encrypt(token!!)
}
val userInfo = userInfoAndToken.first
if (testUserInfo(userInfo)) {
saveAccountAndFinish()
} else {
val intent = Intent(this, EncryptionSettingsActivity::class.java)
intent.putExtra(EncryptionSettingsActivity.EXTRA_USER_INFO, userInfo)
intent.putExtra(EncryptionSettingsActivity.EXTRA_ACCOUNT, account)
startActivityForResult(intent, REQUEST_ENCRYPTION_PASSWORD)
}
}
private fun testUserInfo(userInfo: UserInfoManager.UserInfo?): Boolean {
val encryptionKey = caldavAccount!!.getEncryptionPassword(encryption)
if (userInfo != null && !isNullOrEmpty(encryptionKey)) {
try {
val cryptoManager = CryptoManager(userInfo.version!!.toInt(), encryptionKey, "userInfo")
userInfo.verify(cryptoManager)
return true
} catch (e: IntegrityException) {
Timber.e(e)
} catch (e: VersionTooNewException) {
Timber.e(e)
}
}
return false
}
@OnCheckedChanged(R.id.show_advanced)
fun toggleUrl() {
updateUrlVisibility()
}
private fun updateUrlVisibility() {
binding!!.urlLayout.visibility = if (binding!!.showAdvanced.isChecked) View.VISIBLE else View.GONE
}
override fun needsValidation(): Boolean {
return super.needsValidation() || isNullOrEmpty(caldavAccount!!.encryptionKey)
}
override fun addAccount(url: String?, username: String?, password: String?) {
addAccountViewModel!!.addAccount(eteSyncClient, url, username, password)
}
override fun updateAccount(url: String?, username: String?, password: String?) {
updateAccountViewModel!!.updateAccount(
eteSyncClient,
url,
username,
if (PASSWORD_MASK == password) null else password,
caldavAccount!!.getPassword(encryption))
}
override fun updateAccount() {
caldavAccount!!.name = newName
saveAccountAndFinish()
}
override val newURL: String
get() {
val url = super.newURL
return if (isNullOrEmpty(url)) getString(R.string.etesync_url) else url
}
override val newPassword: String
get() = binding!!.password.text.toString().trim { it <= ' ' }
override val helpUrl: String
get() = "https://tasks.org/etesync"
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_ENCRYPTION_PASSWORD) {
if (resultCode == Activity.RESULT_OK) {
val key = data!!.getStringExtra(EncryptionSettingsActivity.EXTRA_DERIVED_KEY)!!
caldavAccount!!.encryptionKey = encryption.encrypt(key)
saveAccountAndFinish()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun saveAccountAndFinish() {
if (caldavAccount!!.id == Task.NO_ID) {
caldavDao.insert(caldavAccount!!)
} else {
caldavDao.update(caldavAccount!!)
}
setResult(Activity.RESULT_OK)
finish()
}
override fun removeAccount() {
if (caldavAccount != null) {
Completable.fromAction { eteSyncClient.forAccount(caldavAccount!!).invalidateToken() }
.subscribeOn(Schedulers.io())
.subscribe()
}
super.removeAccount()
}
companion object {
private const val REQUEST_ENCRYPTION_PASSWORD = 10101
}
}
Loading…
Cancel
Save