Add EteSyncAccountSettingsActivity

pull/898/head
Alex Baker 4 years ago
parent f97df9e884
commit 744a673c7b

@ -12,6 +12,7 @@ repositories {
jcenter()
google()
maven(url = "https://jitpack.io")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
}
android {
@ -200,6 +201,7 @@ dependencies {
implementation("androidx.work:work-runtime:${Versions.work}")
implementation("com.mapbox.mapboxsdk:mapbox-android-sdk:7.3.0")
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:4.6.0")
implementation("com.etesync:journalmanager:1.0.2-SNAPSHOT")
googleplayImplementation("com.crashlytics.sdk.android:crashlytics:${Versions.crashlytics}")
googleplayImplementation("com.google.firebase:firebase-analytics:${Versions.firebase}")

@ -326,6 +326,10 @@
android:name=".caldav.CaldavAccountSettingsActivity"
android:theme="@style/Tasks"/>
<activity
android:name=".etesync.EteSyncAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".caldav.CaldavCalendarSettingsActivity"
android:theme="@style/Tasks"/>

@ -0,0 +1,29 @@
package org.tasks.activities;
import android.content.Context;
import androidx.core.util.Pair;
import com.etesync.journalmanager.Crypto.AsymmetricKeyPair;
import org.tasks.etesync.EteSyncClient;
import org.tasks.gtasks.PlayServices;
import org.tasks.ui.CompletableViewModel;
public class AddEteSyncAccountViewModel
extends CompletableViewModel<Pair<String, String>> {
public void addAccount(
PlayServices playServices,
Context context,
EteSyncClient client,
String url,
String username,
String password,
String encryptionPassword) {
run(
() -> {
playServices.updateSecurityProvider(context);
return client
.setForeground()
.forUrl(url, username, encryptionPassword, null)
.getKeyAndToken(password);
});
}
}

@ -0,0 +1,23 @@
package org.tasks.activities;
import android.content.Context;
import androidx.core.util.Pair;
import org.tasks.etesync.EteSyncClient;
import org.tasks.ui.CompletableViewModel;
public class UpdateEteSyncAccountViewModel extends CompletableViewModel<Pair<String, String>> {
public void addAccount(
Context context,
EteSyncClient client,
String url,
String username,
String password,
String encryptionPassword) {
run(
() ->
client
.setForeground()
.forUrl(url, username, encryptionPassword, null)
.getKeyAndToken(password));
}
}

@ -30,6 +30,7 @@ import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavDao;
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.etesync.EteSyncAccountSettingsActivity;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.preferences.Preferences;
import org.tasks.security.Encryption;
@ -41,7 +42,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
implements Toolbar.OnMenuItemClickListener {
public static final String EXTRA_CALDAV_DATA = "caldavData"; // $NON-NLS-1$
private static final String PASSWORD_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
protected static final String PASSWORD_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
@Inject protected Tracker tracker;
@Inject protected CaldavDao caldavDao;
@Inject protected Encryption encryption;
@ -71,6 +72,9 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
if (!isEmpty(caldavAccount.getPassword())) {
binding.password.setText(PASSWORD_MASK);
}
if (!isEmpty(caldavAccount.getEncryptionKey())) {
binding.encryptionPassword.setText(PASSWORD_MASK);
}
binding.repeat.setChecked(caldavAccount.isSuppressRepeatingTasks());
}
}
@ -168,7 +172,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
return binding.user.getText().toString().trim();
}
private boolean passwordChanged() {
protected boolean passwordChanged() {
return caldavAccount == null || !PASSWORD_MASK.equals(binding.password.getText().toString().trim());
}
@ -177,6 +181,13 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
return PASSWORD_MASK.equals(input) ? encryption.decrypt(caldavAccount.getPassword()) : input;
}
protected String getNewEncryptionPassword() {
String input = binding.encryptionPassword.getText().toString().trim();
return PASSWORD_MASK.equals(input)
? null
: input;
}
private void save() {
if (requestInProgress()) {
return;
@ -186,6 +197,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
String username = getNewUsername();
String url = getNewURL();
String password = getNewPassword();
String encryptionPassword = getNewEncryptionPassword();
boolean failed = false;
@ -242,6 +254,13 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
failed = true;
}
if (this instanceof EteSyncAccountSettingsActivity) {
if (caldavAccount == null && isEmpty(encryptionPassword)) {
binding.encryptionPassword.setError(getString(R.string.password_required));
failed = true;
}
}
if (failed) {
return;
}
@ -253,7 +272,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
showProgressIndicator();
updateAccount(url, username, password);
} else if (hasChanges()) {
updateAccount(caldavAccount.getUrl());
updateAccount();
} else {
finish();
}
@ -263,20 +282,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
protected abstract void updateAccount(String url, String username, String password);
protected void updateAccount(String principal) {
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();
}
protected abstract void updateAccount();
protected void requestFailed(Throwable t) {
hideProgressIndicator();
@ -321,7 +327,7 @@ public abstract class BaseCaldavAccountSettingsActivity extends ThemedInjectingA
|| binding.repeat.isChecked() != caldavAccount.isSuppressRepeatingTasks();
}
private boolean needsValidation() {
protected boolean needsValidation() {
return !getNewURL().equals(caldavAccount.getUrl())
|| !getNewUsername().equals(caldavAccount.getUsername())
|| passwordChanged();

@ -54,6 +54,21 @@ public class CaldavAccountSettingsActivity extends BaseCaldavAccountSettingsActi
finish();
}
protected void updateAccount(String principal) {
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(playServices, context, client, url, username, password);
@ -64,6 +79,11 @@ public class CaldavAccountSettingsActivity extends BaseCaldavAccountSettingsActi
updateCaldavAccountViewModel.updateCaldavAccount(client, url, username, password);
}
@Override
protected void updateAccount() {
updateAccount(caldavAccount.getUrl());
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);

@ -23,7 +23,7 @@ import org.apache.commons.collections4.map.MultiKeyMap;
* session cookies.
*/
@SuppressWarnings("all")
class MemoryCookieStore implements CookieJar {
public class MemoryCookieStore implements CookieJar {
/**
* Stored cookies. The multi-key consists of three parts: name, domain, and path. This ensures

@ -0,0 +1,126 @@
package org.tasks.etesync;
import android.content.Context;
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.ViewModelProviders;
import butterknife.OnFocusChange;
import butterknife.OnTextChanged;
import com.todoroo.astrid.helper.UUIDHelper;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.AddEteSyncAccountViewModel;
import org.tasks.activities.UpdateEteSyncAccountViewModel;
import org.tasks.analytics.Tracking.Events;
import org.tasks.caldav.BaseCaldavAccountSettingsActivity;
import org.tasks.data.CaldavAccount;
import org.tasks.gtasks.PlayServices;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ForApplication;
public class EteSyncAccountSettingsActivity extends BaseCaldavAccountSettingsActivity
implements Toolbar.OnMenuItemClickListener {
@Inject @ForApplication Context context;
@Inject PlayServices playServices;
@Inject EteSyncClient eteSyncClient;
private AddEteSyncAccountViewModel addAccountViewModel;
private UpdateEteSyncAccountViewModel updateAccountViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.repeat.setVisibility(View.GONE);
binding.encryptionPasswordLayout.setVisibility(View.VISIBLE);
addAccountViewModel = ViewModelProviders.of(this).get(AddEteSyncAccountViewModel.class);
updateAccountViewModel = ViewModelProviders.of(this).get(UpdateEteSyncAccountViewModel.class);
if (savedInstanceState == null) {
if (caldavAccount == null) {
binding.url.setText(R.string.etesync_url);
}
}
addAccountViewModel.observe(this, this::addAccount, this::requestFailed);
updateAccountViewModel.observe(this, this::updateAccount, this::requestFailed);
}
private void addAccount(Pair<String, String> authentication) {
CaldavAccount newAccount = new CaldavAccount();
newAccount.setAccountType(CaldavAccount.TYPE_ETESYNC);
newAccount.setUuid(UUIDHelper.newUUID());
applyTo(newAccount, authentication);
newAccount.setId(caldavDao.insert(newAccount));
tracker.reportEvent(Events.CALDAV_ACCOUNT_ADDED);
setResult(RESULT_OK);
finish();
}
private void updateAccount(Pair<String, String> authentication) {
applyTo(caldavAccount, authentication);
caldavAccount.setError("");
caldavDao.update(caldavAccount);
setResult(RESULT_OK);
finish();
}
private void applyTo(CaldavAccount account, @Nullable Pair<String, String> authentication) {
account.setName(getNewName());
account.setUrl(getNewURL());
account.setUsername(getNewUsername());
if (authentication != null) {
account.setPassword(encryption.encrypt(authentication.first));
account.setEncryptionKey(encryption.encrypt(authentication.second));
}
}
@Override
protected boolean needsValidation() {
return super.needsValidation() || encryptionPasswordChanged();
}
protected boolean encryptionPasswordChanged() {
return caldavAccount == null
|| !PASSWORD_MASK.equals(binding.encryptionPassword.getText().toString().trim());
}
@Override
protected void addAccount(String url, String username, String password) {
addAccountViewModel.addAccount(
playServices, context, eteSyncClient, url, username, password, getNewEncryptionPassword());
}
@Override
protected void updateAccount(String url, String username, String password) {
updateAccountViewModel.addAccount(
context, eteSyncClient, url, username, password, getNewEncryptionPassword());
}
@Override
protected void updateAccount() {
updateAccount(null);
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);
}
@OnTextChanged(R.id.encryption_password)
void onEncryptionPasswordChanged(CharSequence text) {
binding.encryptionPasswordLayout.setError(null);
}
@OnFocusChange(R.id.encryption_password)
void onEncryptionPasswordFocused(boolean hasFocus) {
changePasswordFocus(binding.encryptionPassword, hasFocus);
}
}

@ -0,0 +1,173 @@
package org.tasks.etesync;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier;
import com.etesync.journalmanager.Crypto;
import com.etesync.journalmanager.Crypto.CryptoManager;
import com.etesync.journalmanager.Exceptions;
import com.etesync.journalmanager.Exceptions.IntegrityException;
import com.etesync.journalmanager.Exceptions.VersionTooNewException;
import com.etesync.journalmanager.JournalAuthenticator;
import com.etesync.journalmanager.JournalManager;
import com.etesync.journalmanager.JournalManager.Journal;
import com.etesync.journalmanager.UserInfoManager;
import com.etesync.journalmanager.UserInfoManager.UserInfo;
import com.etesync.journalmanager.model.CollectionInfo;
import com.etesync.journalmanager.util.TokenAuthenticator;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.internal.tls.OkHostnameVerifier;
import org.tasks.DebugNetworkInterceptor;
import org.tasks.caldav.MemoryCookieStore;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import org.tasks.security.Encryption;
import timber.log.Timber;
public class EteSyncClient {
private final Encryption encryption;
private final Preferences preferences;
private final DebugNetworkInterceptor interceptor;
private final String username;
private final String encryptionPassword;
private final OkHttpClient httpClient;
private final HttpUrl httpUrl;
private final Context context;
private final JournalManager journalManager;
private boolean foreground;
@Inject
public EteSyncClient(
@ForApplication Context context,
Encryption encryption,
Preferences preferences,
DebugNetworkInterceptor interceptor) {
this.context = context;
this.encryption = encryption;
this.preferences = preferences;
this.interceptor = interceptor;
username = null;
encryptionPassword = null;
httpClient = null;
httpUrl = null;
journalManager = null;
}
private EteSyncClient(
Context context,
Encryption encryption,
Preferences preferences,
DebugNetworkInterceptor interceptor,
String url,
String username,
String encryptionPassword,
String token,
boolean foreground)
throws NoSuchAlgorithmException, KeyManagementException {
this.context = context;
this.encryption = encryption;
this.preferences = preferences;
this.interceptor = interceptor;
this.username = username;
this.encryptionPassword = encryptionPassword;
CustomCertManager customCertManager = new CustomCertManager(context);
customCertManager.setAppInForeground(foreground);
CustomHostnameVerifier hostnameVerifier =
customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {customCertManager}, null);
Builder builder =
new OkHttpClient()
.newBuilder()
.addNetworkInterceptor(new TokenAuthenticator(null, token))
.cookieJar(new MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.getSocketFactory(), customCertManager)
.hostnameVerifier(hostnameVerifier)
.readTimeout(30, TimeUnit.SECONDS);
if (preferences.isFlipperEnabled()) {
interceptor.add(builder);
}
httpClient = builder.build();
httpUrl = HttpUrl.parse(url);
journalManager = new JournalManager(httpClient, httpUrl);
}
public EteSyncClient forUrl(String url, String username, String encryptionPassword, String token)
throws KeyManagementException, NoSuchAlgorithmException {
return new EteSyncClient(
context,
encryption,
preferences,
interceptor,
url,
username,
encryptionPassword,
token,
foreground);
}
public Pair<String, String> getKeyAndToken(String password)
throws IOException, Exceptions.HttpException, VersionTooNewException, IntegrityException {
JournalAuthenticator journalAuthenticator = new JournalAuthenticator(httpClient, httpUrl);
String token = journalAuthenticator.getAuthToken(username, password);
UserInfoManager userInfoManager = new UserInfoManager(httpClient, httpUrl);
UserInfo userInfo = userInfoManager.fetch(username);
String key = Crypto.deriveKey(username, encryptionPassword);
CryptoManager cryptoManager = new CryptoManager(userInfo.getVersion(), key, "userInfo");
userInfo.verify(cryptoManager);
return Pair.create(token, key);
}
public CryptoManager getCrypto(Journal journal)
throws VersionTooNewException, IntegrityException {
return new CryptoManager(journal.getVersion(), encryptionPassword, journal.getUid());
}
private @Nullable CollectionInfo convertJournalToCollection(Journal journal) {
try {
CryptoManager cryptoManager = getCrypto(journal);
journal.verify(cryptoManager);
CollectionInfo collection =
CollectionInfo.Companion.fromJson(journal.getContent(cryptoManager));
collection.updateFromJournal(journal);
return collection;
} catch (IntegrityException | VersionTooNewException e) {
Timber.e(e);
return null;
}
}
public Map<Journal, CollectionInfo> getCalendars() throws Exceptions.HttpException {
Map<Journal, CollectionInfo> result = new HashMap<>();
for (Journal journal : journalManager.list()) {
CollectionInfo collection = convertJournalToCollection(journal);
if (collection != null && collection.getType().equals("TASKS")) {
result.put(journal, collection);
}
}
return result;
}
public EteSyncClient setForeground() {
foreground = true;
return this;
}
}

@ -19,6 +19,7 @@ import org.tasks.activities.DatePickerActivity;
import org.tasks.activities.FilterSelectionActivity;
import org.tasks.activities.FilterSettingsActivity;
import org.tasks.activities.GoogleTaskListSettingsActivity;
import org.tasks.etesync.EteSyncAccountSettingsActivity;
import org.tasks.tags.TagPickerActivity;
import org.tasks.activities.TagSettingsActivity;
import org.tasks.activities.TimePickerActivity;
@ -134,6 +135,8 @@ public interface ActivityComponent {
void inject(CaldavAccountSettingsActivity caldavAccountSettingsActivity);
void inject(EteSyncAccountSettingsActivity eteSyncAccountSettingsActivity);
void inject(DriveLoginActivity driveLoginActivity);
void inject(DebugPreferences debugPreferences);

@ -17,6 +17,7 @@ import android.preference.Preference;
import android.preference.PreferenceCategory;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.common.base.Strings;
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import com.todoroo.astrid.service.TaskDeleter;
import javax.inject.Inject;
@ -26,6 +27,7 @@ import org.tasks.analytics.Tracking;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseActivity;
import org.tasks.caldav.CaldavAccountSettingsActivity;
import org.tasks.etesync.EteSyncAccountSettingsActivity;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskAccount;
@ -48,6 +50,8 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
private static final int REQUEST_CALDAV_SETTINGS = 101;
private static final int REQUEST_CALDAV_SUBSCRIBE = 102;
private static final int REQUEST_GOOGLE_TASKS_SUBSCRIBE = 103;
private static final int REQUEST_ETESYNC_SETTINGS = 104;
private static final int REQUEST_ETESYNC_SUBSCRIBE = 105;
@Inject ActivityPermissionRequestor permissionRequestor;
@Inject PermissionChecker permissionChecker;
@ -130,6 +134,23 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
return false;
});
}
Preference addEteSyncAccount = findPreference(R.string.p_add_etesync_account);
if (inventory.hasPro()) {
addEteSyncAccount.setOnPreferenceClickListener(
preference -> {
addEteSyncAccount();
return false;
});
} else {
addEteSyncAccount.setSummary(R.string.requires_pro_subscription);
addEteSyncAccount.setOnPreferenceClickListener(
preference -> {
startActivityForResult(
new Intent(this, PurchaseActivity.class), REQUEST_ETESYNC_SUBSCRIBE);
return false;
});
}
}
private void logoutConfirmation(GoogleTaskAccount account) {
@ -201,19 +222,34 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
private void addCaldavAccounts() {
PreferenceCategory caldavPreferences =
(PreferenceCategory) findPreference(getString(R.string.CalDAV));
PreferenceCategory eteSyncPreferences =
(PreferenceCategory) findPreference(R.string.etesync);
caldavPreferences.removeAll();
eteSyncPreferences.removeAll();
for (CaldavAccount caldavAccount : caldavDao.getAccounts()) {
Preference accountPreferences = new Preference(this);
accountPreferences.setTitle(caldavAccount.getName());
accountPreferences.setSummary(caldavAccount.getError());
accountPreferences.setOnPreferenceClickListener(
preference -> {
Intent intent = new Intent(this, CaldavAccountSettingsActivity.class);
intent.putExtra(CaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, caldavAccount);
startActivityForResult(intent, REQUEST_CALDAV_SETTINGS);
return false;
});
caldavPreferences.addPreference(accountPreferences);
if (caldavAccount.isCaldavAccount()) {
accountPreferences.setOnPreferenceClickListener(
preference -> {
Intent intent = new Intent(this, CaldavAccountSettingsActivity.class);
intent.putExtra(CaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, caldavAccount);
startActivityForResult(intent, REQUEST_CALDAV_SETTINGS);
return false;
});
caldavPreferences.addPreference(accountPreferences);
} else if (caldavAccount.isEteSyncAccount()) {
accountPreferences.setOnPreferenceClickListener(
preference -> {
Intent intent = new Intent(this, EteSyncAccountSettingsActivity.class);
intent.putExtra(CaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, caldavAccount);
startActivityForResult(intent, REQUEST_ETESYNC_SETTINGS);
return false;
}
);
eteSyncPreferences.addPreference(accountPreferences);
}
}
}
@ -222,6 +258,11 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
new Intent(this, CaldavAccountSettingsActivity.class), REQUEST_CALDAV_SETTINGS);
}
private void addEteSyncAccount() {
startActivityForResult(
new Intent(this, EteSyncAccountSettingsActivity.class), REQUEST_ETESYNC_SETTINGS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LOGIN) {
@ -233,7 +274,7 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
} else if (data != null) {
toaster.longToast(data.getStringExtra(GtasksLoginActivity.EXTRA_ERROR));
}
} else if (requestCode == REQUEST_CALDAV_SETTINGS) {
} else if (requestCode == REQUEST_CALDAV_SETTINGS || requestCode == REQUEST_ETESYNC_SETTINGS) {
if (resultCode == RESULT_OK) {
workManager.updateBackgroundSync();
restart();
@ -246,6 +287,10 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
if (inventory.hasPro()) {
requestLogin();
}
} else if (requestCode == REQUEST_ETESYNC_SUBSCRIBE) {
if (inventory.hasPro()) {
addEteSyncAccount();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}

@ -80,6 +80,22 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/encryption_password_layout"
style="@style/TagSettingsRow"
android:visibility="gone"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/encryption_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/encryption_password"
android:imeOptions="flagNoExtractUi"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/repeat"
style="@style/TagSettingsRow"

@ -6,6 +6,8 @@
<resources>
<string name="FSA_label">Tasks Shortcut</string>
<string name="etesync">EteSync</string>
<string name="etesync_url">https://api.etesync.com</string>
<string name="p_date_shortcut_morning">date_shortcut_morning</string>
<string name="p_date_shortcut_afternoon">date_shortcut_afternoon</string>
@ -303,6 +305,7 @@
<string name="preference_screen">preference_screen</string>
<string name="p_add_google_task_account">add_google_task_account</string>
<string name="p_add_caldav_account">add_caldav_account</string>
<string name="p_add_etesync_account">add_etesync_account</string>
<string name="p_google_tasks_add_to_top">google_tasks_add_to_top</string>
<string name="p_google_tasks_position_hack">google_tasks_position_hack</string>
<string name="google_tasks_position_hack">Custom order synchronization fix</string>

@ -378,6 +378,7 @@ File %1$s contained %2$s.\n\n
<string name="name_cannot_be_empty">Name cannot be empty</string>
<string name="username_required">Username required</string>
<string name="password_required">Password required</string>
<string name="encryption_password_required">Encryption password required</string>
<string name="url_required">URL required</string>
<string name="url_host_name_required">Host name required</string>
<string name="url_invalid_scheme">Must begin with http(s)://</string>
@ -447,6 +448,7 @@ File %1$s contained %2$s.\n\n
<string name="add_account">Add account</string>
<string name="user">User</string>
<string name="password">Password</string>
<string name="encryption_password">Encryption password</string>
<string name="url">URL</string>
<string name="error_adding_account">Error: %s</string>
<string name="calendar_settings">Calendar settings</string>

@ -31,6 +31,14 @@
android:key="@string/p_add_caldav_account"
android:title="@string/add_account"/>
<PreferenceCategory
android:key="@string/etesync"
android:title="@string/etesync" />
<Preference
android:key="@string/p_add_etesync_account"
android:title="@string/add_account"/>
<PreferenceCategory android:title="@string/sync_SPr_interval_title">
<CheckBoxPreference
android:defaultValue="true"

Loading…
Cancel
Save