diff --git a/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.kt b/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.kt index bd5795b91..6932278a7 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/dao/TaskDaoTests.kt @@ -11,6 +11,7 @@ import com.todoroo.astrid.data.Task import com.todoroo.astrid.service.TaskDeleter import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules +import kotlinx.coroutines.runBlocking import org.junit.Assert.* import org.junit.Test import org.tasks.injection.InjectingTestCase @@ -106,7 +107,7 @@ class TaskDaoTests : InjectingTestCase() { /** Test task deletion */ @Test - fun testTDeletion() { + fun testTDeletion() = runBlocking { assertEquals(0, taskDao.getAll().size) // create task "happy" @@ -133,7 +134,7 @@ class TaskDaoTests : InjectingTestCase() { /** Test passing invalid task indices to various things */ @Test - fun testInvalidIndex() { + fun testInvalidIndex() = runBlocking { assertEquals(0, taskDao.getAll().size) assertNull(taskDao.fetchBlocking(1)) taskDeleter.delete(listOf(1L)) diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.java b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.java deleted file mode 100644 index d8a51ba5d..000000000 --- a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt new file mode 100644 index 000000000..0b11ca055 --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java b/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java deleted file mode 100644 index ba3fdc43b..000000000 --- a/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java +++ /dev/null @@ -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"; - } -} diff --git a/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.kt new file mode 100644 index 000000000..c865e64a7 --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.kt @@ -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" +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.java b/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.java deleted file mode 100644 index e4c0a5e7b..000000000 --- a/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.java +++ /dev/null @@ -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 userInfoAndToken) { - caldavAccount = new CaldavAccount(); - caldavAccount.setAccountType(CaldavAccount.TYPE_ETESYNC); - caldavAccount.setUuid(UUIDHelper.newUUID()); - applyTo(caldavAccount, userInfoAndToken); - } - - private void updateAccount(Pair userInfoAndToken) { - caldavAccount.setError(""); - applyTo(caldavAccount, userInfoAndToken); - } - - private void applyTo(CaldavAccount account, Pair 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(); - } -} diff --git a/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.kt b/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.kt new file mode 100644 index 000000000..3b2e269e1 --- /dev/null +++ b/app/src/main/java/org/tasks/etesync/EteSyncAccountSettingsActivity.kt @@ -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 -> this.addAccount(userInfoAndToken) }, Observer { t: Throwable? -> requestFailed(t!!) }) + updateAccountViewModel!!.observe(this, Observer { userInfoAndToken: Pair -> 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) { + caldavAccount = CaldavAccount() + caldavAccount!!.accountType = CaldavAccount.TYPE_ETESYNC + caldavAccount!!.uuid = UUIDHelper.newUUID() + applyTo(caldavAccount!!, userInfoAndToken) + } + + private fun updateAccount(userInfoAndToken: Pair) { + caldavAccount!!.error = "" + applyTo(caldavAccount!!, userInfoAndToken) + } + + private fun applyTo(account: CaldavAccount, userInfoAndToken: Pair) { + 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 + } +} \ No newline at end of file