Update caldav account settings

* Encrypt passwords (Android 6+)
* Add name field
pull/699/head
Alex Baker 6 years ago
parent f8fab59be9
commit 57aec936fc

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 58,
"identityHash": "119477b5c2bd3389e53fef40e4844e01",
"identityHash": "dad699968028cb1d91c793e29b73ceb9",
"entities": [
{
"tableName": "notification",
@ -761,7 +761,7 @@
"notNull": true
},
{
"fieldPath": "account",
"fieldPath": "calendar",
"columnName": "calendar",
"affinity": "TEXT",
"notNull": false
@ -814,7 +814,7 @@
},
{
"tableName": "caldav_account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT, `iv` BLOB)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT)",
"fields": [
{
"fieldPath": "id",
@ -851,12 +851,6 @@
"columnName": "password",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "iv",
"columnName": "iv",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
@ -871,7 +865,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"119477b5c2bd3389e53fef40e4844e01\")"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"dad699968028cb1d91c793e29b73ceb9\")"
]
}
}

@ -179,6 +179,10 @@ public class AndroidUtilities {
return !atLeastJellybean();
}
public static boolean preMarshmallow() {
return !atLeastMarshmallow();
}
public static boolean preOreo() {
return !atLeastOreo();
}

@ -335,7 +335,7 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
if (!inventory.hasPro()) {
add(
new NavigationDrawerAction(
activity.getResources().getString(R.string.subscribe_to_pro),
activity.getResources().getString(R.string.upgrade_to_pro),
R.drawable.ic_attach_money_black_24dp,
new Intent(activity, PurchaseActivity.class),
REQUEST_PURCHASE));

@ -1,6 +1,7 @@
package org.tasks.caldav;
import static android.text.TextUtils.isEmpty;
import static com.todoroo.andlib.utility.AndroidUtilities.preMarshmallow;
import android.app.ProgressDialog;
import android.content.Context;
@ -14,10 +15,7 @@ import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.PropertyCollection;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.DisplayName;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnFocusChange;
@ -28,7 +26,6 @@ import java.net.ConnectException;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.analytics.Tracker;
@ -39,8 +36,10 @@ import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.preferences.Preferences;
import org.tasks.security.Encryption;
import org.tasks.sync.SyncAdapters;
import org.tasks.ui.DisplayableException;
import org.tasks.ui.MenuColorizer;
import timber.log.Timber;
public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActivity
@ -54,10 +53,14 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
@Inject CaldavDao caldavDao;
@Inject SyncAdapters syncAdapters;
@Inject TaskDeleter taskDeleter;
@Inject Encryption encryption;
@BindView(R.id.root_layout)
LinearLayout root;
@BindView(R.id.name)
TextInputEditText name;
@BindView(R.id.url)
TextInputEditText url;
@ -67,6 +70,9 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
@BindView(R.id.password)
TextInputEditText password;
@BindView(R.id.name_layout)
TextInputLayout nameLayout;
@BindView(R.id.url_layout)
TextInputLayout urlLayout;
@ -92,7 +98,13 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
caldavAccount = getIntent().getParcelableExtra(EXTRA_CALDAV_DATA);
if (savedInstanceState == null) {
if (caldavAccount == null) {
if (preMarshmallow()) {
passwordLayout.setError(getString(R.string.encryption_warning));
}
}
if (caldavAccount != null) {
name.setText(caldavAccount.getName());
url.setText(caldavAccount.getUrl());
user.setText(caldavAccount.getUsername());
if (!isEmpty(caldavAccount.getPassword())) {
@ -118,15 +130,21 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
toolbar.inflateMenu(R.menu.menu_caldav_account_settings);
toolbar.setOnMenuItemClickListener(this);
toolbar.showOverflowMenu();
MenuColorizer.colorToolbar(this, toolbar);
if (caldavAccount == null) {
toolbar.getMenu().findItem(R.id.remove).setVisible(false);
url.requestFocus();
name.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(url, InputMethodManager.SHOW_IMPLICIT);
imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT);
}
}
@OnTextChanged(R.id.name)
void onNameChanged(CharSequence text) {
nameLayout.setError(null);
}
@OnTextChanged(R.id.url)
void onUrlChanged(CharSequence text) {
urlLayout.setError(null);
@ -149,7 +167,7 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
password.setText("");
}
} else {
if (isEmpty(password.getText()) && !isEmpty(caldavAccount.getPassword())) {
if (isEmpty(password.getText()) && caldavAccount != null) {
password.setText(PASSWORD_MASK);
}
}
@ -160,6 +178,10 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
component.inject(this);
}
private String getNewName() {
return name.getText().toString().trim();
}
private String getNewURL() {
return url.getText().toString().trim();
}
@ -168,18 +190,36 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
return user.getText().toString().trim();
}
private boolean passwordChanged() {
return caldavAccount == null || !PASSWORD_MASK.equals(password.getText().toString().trim());
}
private String getNewPassword() {
String input = password.getText().toString().trim();
return PASSWORD_MASK.equals(input) ? caldavAccount.getPassword() : input;
return PASSWORD_MASK.equals(input)
? encryption.decrypt(caldavAccount.getPassword())
: input;
}
private void save() {
String name = getNewName();
String username = getNewUsername();
String url = getNewURL();
String password = getNewPassword();
boolean failed = false;
if (isEmpty(name)) {
nameLayout.setError(getString(R.string.name_cannot_be_empty));
failed = true;
} else {
CaldavAccount accountByName = caldavDao.getAccountByName(name);
if (accountByName != null && !accountByName.equals(caldavAccount)) {
nameLayout.setError(getString(R.string.duplicate_name));
failed = true;
}
}
if (isEmpty(url)) {
urlLayout.setError(getString(R.string.url_required));
failed = true;
@ -255,7 +295,7 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
CaldavAccount newAccount = new CaldavAccount();
newAccount.setUrl(principal);
newAccount.setUsername(getNewUsername());
newAccount.setPassword(getNewPassword());
newAccount.setPassword(encryption.encrypt(getNewPassword()));
newAccount.setUuid(UUIDHelper.newUUID());
newAccount.setId(caldavDao.insert(newAccount));
@ -264,9 +304,12 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
}
private void updateAccount(String principal) {
caldavAccount.setName(getNewName());
caldavAccount.setUrl(principal);
caldavAccount.setUsername(getNewUsername());
caldavAccount.setPassword(getNewPassword());
if (passwordChanged()) {
caldavAccount.setPassword(encryption.encrypt(getNewPassword()));
}
caldavDao.update(caldavAccount);
setResult(RESULT_OK);
@ -305,21 +348,24 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv
private boolean hasChanges() {
if (caldavAccount == null) {
return !isEmpty(getNewPassword()) || !isEmpty(getNewURL()) || !isEmpty(getNewUsername());
return !isEmpty(getNewName())
|| !isEmpty(getNewPassword())
|| !isEmpty(getNewURL())
|| !isEmpty(getNewUsername());
}
return needsValidation();
return needsValidation() || !getNewName().equals(caldavAccount.getName());
}
private boolean needsValidation() {
return !getNewURL().equals(caldavAccount.getUrl())
|| !getNewUsername().equals(caldavAccount.getUsername())
|| !getNewPassword().equals(caldavAccount.getPassword());
|| passwordChanged();
}
@Override
public void finish() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(url.getWindowToken(), 0);
imm.hideSoftInputFromWindow(name.getWindowToken(), 0);
super.finish();
}

@ -27,6 +27,8 @@ import java.util.concurrent.TimeUnit;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import org.tasks.R;
import org.tasks.data.CaldavAccount;
import org.tasks.security.Encryption;
import org.tasks.ui.DisplayableException;
import timber.log.Timber;
@ -35,7 +37,14 @@ class CaldavClient {
private final DavResource davResource;
private HttpUrl httpUrl;
public CaldavClient(String url, String username, String password) {
CaldavClient(CaldavAccount caldavAccount, Encryption encryption) {
this(
caldavAccount.getUrl(),
caldavAccount.getUsername(),
encryption.decrypt(caldavAccount.getPassword()));
}
CaldavClient(String url, String username, String password) {
BasicDigestAuthHandler basicDigestAuthHandler =
new BasicDigestAuthHandler(null, username, password);
OkHttpClient httpClient =

@ -24,6 +24,7 @@ import at.bitfire.dav4android.property.GetETag;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.ical4android.iCalendar;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.CharStreams;
import com.todoroo.andlib.utility.DateUtilities;
@ -55,6 +56,7 @@ import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.injection.ForApplication;
import org.tasks.security.Encryption;
import timber.log.Timber;
public class CaldavSynchronizer {
@ -69,6 +71,7 @@ public class CaldavSynchronizer {
private final LocalBroadcastManager localBroadcastManager;
private final TaskCreator taskCreator;
private final TaskDeleter taskDeleter;
private final Encryption encryption;
private final Context context;
@Inject
@ -78,25 +81,31 @@ public class CaldavSynchronizer {
TaskDao taskDao,
LocalBroadcastManager localBroadcastManager,
TaskCreator taskCreator,
TaskDeleter taskDeleter) {
TaskDeleter taskDeleter,
Encryption encryption) {
this.context = context;
this.caldavDao = caldavDao;
this.taskDao = taskDao;
this.localBroadcastManager = localBroadcastManager;
this.taskCreator = taskCreator;
this.taskDeleter = taskDeleter;
this.encryption = encryption;
}
public void sync() {
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(context.getClassLoader());
for (CaldavAccount account : caldavDao.getAccounts()) {
CaldavClient caldavClient =
new CaldavClient(account.getUrl(), account.getUsername(), account.getPassword());
if (isNullOrEmpty(account.getPassword())) {
Timber.e("Missing password for %s", account);
continue;
}
CaldavClient caldavClient = new CaldavClient(account, encryption);
List<DavResource> resources = caldavClient.getCalendars();
Set<String> urls = newHashSet(transform(resources, c -> c.getLocation().toString()));
Timber.d("Found calendars: %s", urls);
for (CaldavCalendar deleted : caldavDao.findDeletedCalendars(account.getUuid(), newArrayList(urls))) {
for (CaldavCalendar deleted :
caldavDao.findDeletedCalendars(account.getUuid(), newArrayList(urls))) {
taskDeleter.markDeleted(caldavDao.getTasksByCalendar(deleted.getUuid()));
caldavDao.deleteTasksForCalendar(deleted.getUuid());
caldavDao.delete(deleted);
@ -115,22 +124,15 @@ public class CaldavSynchronizer {
calendar.setId(caldavDao.insert(calendar));
localBroadcastManager.broadcastRefreshList();
}
String ctag = properties.get(GetCTag.class).getCTag();
if (calendar.getCtag() == null || !calendar.getCtag().equals(ctag)) {
sync(account, calendar);
}
sync(account, calendar);
}
}
}
private void sync(CaldavAccount account, CaldavCalendar caldavCalendar) {
if (isNullOrEmpty(account.getPassword())) {
Timber.e("Missing password for %s", caldavCalendar);
return;
}
Timber.d("sync(%s)", caldavCalendar);
BasicDigestAuthHandler basicDigestAuthHandler =
new BasicDigestAuthHandler(null, account.getUsername(), account.getPassword());
new BasicDigestAuthHandler(null, account.getUsername(), encryption.decrypt(account.getPassword()));
OkHttpClient httpClient =
new OkHttpClient()
.newBuilder()
@ -173,7 +175,7 @@ public class CaldavSynchronizer {
Iterable<DavResource> changed =
filter(
davCalendar.getMembers(),
ImmutableSet.copyOf(davCalendar.getMembers()),
vCard -> {
GetETag eTag = (GetETag) vCard.getProperties().get(GetETag.NAME);
if (eTag == null || isNullOrEmpty(eTag.getETag())) {

@ -87,8 +87,8 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da
new ExtensionData()
.visible(true)
.icon(R.drawable.ic_check_white_24dp)
.status(getString(R.string.subscribe_to_pro))
.expandedTitle(getString(R.string.subscribe_to_pro))
.status(getString(R.string.upgrade_to_pro))
.expandedTitle(getString(R.string.upgrade_to_pro))
.clickIntent(new Intent(this, DashClockSettings.class)));
}
}

@ -46,9 +46,6 @@ public class CaldavAccount implements Parcelable {
@ColumnInfo(name = "password")
private transient String password = "";
@ColumnInfo(name = "iv")
private transient byte[] iv = null;
public CaldavAccount() {}
@Ignore
@ -59,7 +56,6 @@ public class CaldavAccount implements Parcelable {
url = source.readString();
username = source.readString();
password = source.readString();
iv = source.createByteArray();
}
public long getId() {
@ -110,14 +106,6 @@ public class CaldavAccount implements Parcelable {
this.password = password;
}
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
@Override
public String toString() {
return "CaldavAccount{" +
@ -127,7 +115,6 @@ public class CaldavAccount implements Parcelable {
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", iv=" + Arrays.toString(iv) +
'}';
}
@ -157,10 +144,7 @@ public class CaldavAccount implements Parcelable {
if (username != null ? !username.equals(that.username) : that.username != null) {
return false;
}
if (password != null ? !password.equals(that.password) : that.password != null) {
return false;
}
return Arrays.equals(iv, that.iv);
return password != null ? password.equals(that.password) : that.password == null;
}
@Override
@ -171,7 +155,6 @@ public class CaldavAccount implements Parcelable {
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (username != null ? username.hashCode() : 0);
result = 31 * result + (password != null ? password.hashCode() : 0);
result = 31 * result + Arrays.hashCode(iv);
return result;
}
@ -188,6 +171,5 @@ public class CaldavAccount implements Parcelable {
dest.writeString(url);
dest.writeString(username);
dest.writeString(password);
dest.writeByteArray(iv);
}
}

@ -90,4 +90,7 @@ public interface CaldavDao {
@Query("SELECT * FROM caldav_calendar WHERE account = :account AND url = :url LIMIT 1")
CaldavCalendar getCalendarByUrl(String account, String url);
@Query("SELECT * FROM caldav_account WHERE name = :name COLLATE NOCASE LIMIT 1")
CaldavAccount getAccountByName(String name);
}

@ -1,20 +0,0 @@
package org.tasks.security;
public class EncryptedString {
private final String value;
private final byte[] iv;
public EncryptedString(String value, byte[] iv) {
this.value = value;
this.iv = iv;
}
public String getValue() {
return value;
}
public byte[] getIv() {
return iv;
}
}

@ -1,15 +1,8 @@
package org.tasks.security;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
public interface Encryption {
EncryptedString encrypt(String text)
throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException;
String encrypt(String text);
String decrypt(EncryptedString encryptedString)
throws IOException, BadPaddingException, IllegalBlockSizeException;
String decrypt(String text);
}

@ -5,8 +5,10 @@ import android.os.Build.VERSION_CODES;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.RequiresApi;
import android.util.Base64;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
@ -14,8 +16,10 @@ import java.security.KeyStore.Entry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@ -24,13 +28,16 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.inject.Inject;
import timber.log.Timber;
@RequiresApi(api = VERSION_CODES.M)
public class KeyStoreEncryption implements Encryption {
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String ALIAS = "passwords";
private static final String ENCODING = "UTF-8";
private static final Charset ENCODING = StandardCharsets.UTF_8;
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private KeyStore keyStore;
@ -45,28 +52,40 @@ public class KeyStoreEncryption implements Encryption {
}
@Override
public EncryptedString encrypt(String text)
throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, null);
return new EncryptedString(
new String(cipher.doFinal(text.getBytes(ENCODING)), ENCODING), cipher.getIV());
public String encrypt(String text) {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, iv);
try {
byte[] output = cipher.doFinal(text.getBytes(ENCODING));
byte[] result = new byte[iv.length + output.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(output, 0, result, iv.length, output.length);
return Base64.encodeToString(result, Base64.DEFAULT);
} catch (IllegalBlockSizeException | BadPaddingException e) {
Timber.e(e);
return null;
}
}
@Override
public String decrypt(EncryptedString encryptedString)
throws IOException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, encryptedString.getIv());
return new String(cipher.doFinal(encryptedString.getValue().getBytes(ENCODING)), ENCODING);
public String decrypt(String text) {
byte[] decoded = Base64.decode(text, Base64.DEFAULT);
byte[] iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH);
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, iv);
try {
byte[] decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.length - GCM_IV_LENGTH);
return new String(decrypted, ENCODING);
} catch (IllegalBlockSizeException | BadPaddingException e) {
Timber.e(e);
return null;
}
}
private Cipher getCipher(int cipherMode, byte[] iv) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
if (cipherMode == Cipher.ENCRYPT_MODE) {
cipher.init(cipherMode, getSecretKey());
} else {
cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(128, iv));
}
cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv));
return cipher;
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
@ -95,6 +114,7 @@ public class KeyStoreEncryption implements Encryption {
new KeyGenParameterSpec.Builder(
ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setRandomizedEncryptionRequired(false)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build());
return keyGenerator.generateKey();

@ -1,21 +1,14 @@
package org.tasks.security;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
public class NoEncryption implements Encryption {
@Override
public EncryptedString encrypt(String text)
throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
return new EncryptedString(text, null);
public String encrypt(String text) {
return text;
}
@Override
public String decrypt(EncryptedString encryptedString)
throws IOException, BadPaddingException, IllegalBlockSizeException {
return encryptedString.getValue();
public String decrypt(String text) {
return text;
}
}

@ -65,6 +65,7 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
for (CaldavAccount caldavAccount : caldavDao.getAccounts()) {
Preference accountPreferences = new Preference(this);
accountPreferences.setTitle(caldavAccount.getName());
accountPreferences.setSummary(caldavAccount.getUrl());
accountPreferences.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(this, CaldavAccountSettingsActivity.class);
intent.putExtra(CaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, caldavAccount);
@ -73,12 +74,14 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity {
});
caldavPreferences.addPreference(accountPreferences);
}
findPreference(getString(R.string.add_account)).setOnPreferenceClickListener(
preference -> {
startActivityForResult(new Intent(this, CaldavAccountSettingsActivity.class),
REQUEST_CALDAV_SETTINGS);
return false;
});
Preference addCaldavAccount = new Preference(this);
addCaldavAccount.setTitle(R.string.add_account);
addCaldavAccount.setOnPreferenceClickListener(preference -> {
startActivityForResult(new Intent(this, CaldavAccountSettingsActivity.class),
REQUEST_CALDAV_SETTINGS);
return false;
});
caldavPreferences.addPreference(addCaldavAccount);
final CheckBoxPreference gtaskPreference =
(CheckBoxPreference) findPreference(getString(R.string.sync_gtasks));

@ -20,6 +20,20 @@
android:focusableInTouchMode="true"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:id="@+id/name_layout"
style="@style/TagSettingsRow">
<android.support.design.widget.TextInputEditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/name"
android:imeOptions="flagNoExtractUi"
android:textColor="?attr/asTextColor"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/url_layout"
style="@style/TagSettingsRow">

@ -752,6 +752,7 @@ File %1$s contained %2$s.\n\n
<string name="send_anonymous_statistics">Improve Tasks</string>
<string name="send_anonymous_statistics_summary">Send anonymous usage statistics and crash reports to help improve Tasks. No personal data will be collected.</string>
<string name="tag_already_exists">Tag already exists</string>
<string name="duplicate_name">Duplicate name</string>
<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>
@ -882,7 +883,7 @@ File %1$s contained %2$s.\n\n
<string name="network_error">Connection failed</string>
<string name="background_sync_unmetered_only">Only on unmetered connections</string>
<string name="upgrade">Upgrade</string>
<string name="subscribe_to_pro">Subscribe to pro</string>
<string name="upgrade_to_pro">Upgrade to pro</string>
<string name="refresh_purchases">Refresh purchases</string>
<string name="button_subscribed">Subscribed</string>
<string name="button_subscribe">Subscribe</string>
@ -898,5 +899,6 @@ File %1$s contained %2$s.\n\n
<string name="pro_multiple_google_task_accounts">Multiple Google Task accounts</string>
<string name="pro_tasker_plugins">Tasker plugins</string>
<string name="pro_dashclock_extension">Dashclock extension</string>
<string name="encryption_warning">Passwords are stored in plain text on devices running Android 5 or below. This is a security concern if your device has been rooted.</string>
</resources>

@ -24,11 +24,7 @@
<PreferenceCategory
android:key="@string/CalDAV"
android:title="@string/CalDAV">
<Preference
android:key="@string/add_account"
android:title="@string/add_account"/>
</PreferenceCategory>
android:title="@string/CalDAV"/>
<PreferenceCategory android:title="@string/sync_SPr_interval_title">
<CheckBoxPreference

Loading…
Cancel
Save