Provide settings migration v0.9 -> v1.0

pull/2/head
Ricki Hirner 8 years ago
parent 1df3ddbe74
commit 1786b73ac6

@ -9,7 +9,6 @@ package at.bitfire.davdroid;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ContentProviderClient;
@ -19,35 +18,44 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.PeriodicSync;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.*;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import okhttp3.HttpUrl;
public class AccountSettings {
private final static int CURRENT_VERSION = 2;
private final static int CURRENT_VERSION = 3;
private final static String
KEY_SETTINGS_VERSION = "version",
KEY_USERNAME = "user_name",
KEY_AUTH_PREEMPTIVE = "auth_preemptive",
KEY_LAST_ANDROID_VERSION = "last_android_version";
KEY_USERNAME = "user_name",
KEY_AUTH_PREEMPTIVE = "auth_preemptive";
/* Time range limitation to the past [days]
/** Time range limitation to the past [in days]
value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
< 0 (-1) no limit
>= 0 entries more than n days in the past won't be synchronized
@ -62,7 +70,7 @@ public class AccountSettings {
final Account account;
public AccountSettings(Context context, Account account) {
public AccountSettings(@NonNull Context context, @NonNull Account account) {
this.context = context;
this.account = account;
@ -74,46 +82,25 @@ public class AccountSettings {
version = Integer.parseInt(accountManager.getUserData(account, KEY_SETTINGS_VERSION));
} catch(NumberFormatException ignored) {
}
App.log.info("AccountSettings version: v" + version + ", should be: " + version);
App.log.info("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION);
if (version < CURRENT_VERSION) {
showNotification(Constants.NOTIFICATION_ACCOUNT_SETTINGS_UPDATED,
context.getString(R.string.settings_version_update_title),
context.getString(R.string.settings_version_update_description));
update(version);
}
Notification notify = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(context.getString(R.string.settings_version_update))
.setContentText(context.getString(R.string.settings_version_update_warning))
.setCategory(NotificationCompat.CATEGORY_SYSTEM)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setLocalOnly(true)
.build();
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(Constants.NOTIFICATION_ACCOUNT_SETTINGS_UPDATED, notify);
// check whether Android version has changed
String lastAndroidVersionInt = accountManager.getUserData(account, KEY_LAST_ANDROID_VERSION);
if (lastAndroidVersionInt != null && NumberUtils.toInt(lastAndroidVersionInt) < Build.VERSION.SDK_INT) {
// notify user
showNotification(Constants.NOTIFICATION_ANDROID_VERSION_UPDATED,
context.getString(R.string.settings_android_update_title),
context.getString(R.string.settings_android_update_description));
update(version);
}
accountManager.setUserData(account, KEY_LAST_ANDROID_VERSION, String.valueOf(Build.VERSION.SDK_INT));
}
}
@TargetApi(21)
protected void showNotification(int id, String title, String message) {
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder n = new Notification.Builder(context);
if (Build.VERSION.SDK_INT >= 16) {
n.setPriority(Notification.PRIORITY_HIGH);
n.setStyle(new Notification.BigTextStyle().bigText(message));
} if (Build.VERSION.SDK_INT >= 20)
n.setLocalOnly(true);
if (Build.VERSION.SDK_INT >= 21)
n.setCategory(Notification.CATEGORY_SYSTEM);
n.setSmallIcon(R.drawable.ic_launcher);
n.setContentTitle(title);
n.setContentText(message);
nm.notify(id, Build.VERSION.SDK_INT >= 16 ? n.build() : n.getNotification());
}
public static Bundle initialUserData(String userName, boolean preemptive) {
Bundle bundle = new Bundle();
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
@ -126,10 +113,10 @@ public class AccountSettings {
// authentication settings
public String username() { return accountManager.getUserData(account, KEY_USERNAME); }
public void username(String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
public void username(@NonNull String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
public String password() { return accountManager.getPassword(account); }
public void password(String password) { accountManager.setPassword(account, password); }
public void password(@NonNull String password) { accountManager.setPassword(account, password); }
public boolean preemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
public void preemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
@ -137,7 +124,7 @@ public class AccountSettings {
// sync. settings
public Long getSyncInterval(String authority) {
public Long getSyncInterval(@NonNull String authority) {
if (ContentResolver.getIsSyncable(account, authority) <= 0)
return null;
@ -151,7 +138,7 @@ public class AccountSettings {
return SYNC_INTERVAL_MANUALLY;
}
public void setSyncInterval(String authority, long seconds) {
public void setSyncInterval(@NonNull String authority, long seconds) {
if (seconds == SYNC_INTERVAL_MANUALLY) {
ContentResolver.setSyncAutomatically(account, authority, false);
} else {
@ -177,30 +164,19 @@ public class AccountSettings {
// update from previous account settings
private void update(int fromVersion) {
for (int toVersion = fromVersion + 1; toVersion <= CURRENT_VERSION; toVersion++)
updateTo(toVersion);
}
private void updateTo(int toVersion) {
final int fromVersion = toVersion - 1;
App.log.info("Updating account settings from v" + fromVersion + " to " + toVersion);
try {
switch (toVersion) {
case 1:
update_0_1();
break;
case 2:
update_1_2();
break;
default:
App.log.severe("Don't know how to update settings from v" + fromVersion + " to v" + toVersion);
for (int toVersion = fromVersion + 1; toVersion <= CURRENT_VERSION; toVersion++) {
App.log.info("Updating account " + account.name + " from version " + fromVersion + " to " + toVersion);
try {
Method updateProc = getClass().getDeclaredMethod("update_" + fromVersion + "_" + toVersion);
updateProc.invoke(this);
} catch (Exception e) {
App.log.log(Level.SEVERE, "Couldn't update account settings", e);
}
} catch(Exception e) {
App.log.log(Level.SEVERE, "Couldn't update account settings (DAVdroid will probably crash)!", e);
}
fromVersion = toVersion;
}
}
@SuppressWarnings("Recycle")
@SuppressWarnings({ "Recycle", "unused" })
private void update_0_1() throws URISyntaxException {
String v0_principalURL = accountManager.getUserData(account, "principal_url"),
v0_addressBookPath = accountManager.getUserData(account, "addressbook_path");
@ -244,7 +220,7 @@ public class AccountSettings {
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "1");
}
@SuppressWarnings("Recycle")
@SuppressWarnings({ "Recycle", "unused" })
private void update_1_2() throws ContactsStorageException {
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
@ -277,4 +253,117 @@ public class AccountSettings {
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "2");
}
@SuppressWarnings({ "Recycle", "unused" })
private void update_2_3() {
// Don't show a warning for Android updates anymore
accountManager.setUserData(account, "last_android_version", null);
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
try {
SQLiteDatabase db = dbHelper.getWritableDatabase();
// we have to create the WebDAV Service database only from the old address book, calendar and task list URLs
// CardDAV: migrate address books
ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (client != null)
try {
LocalAddressBook addrBook = new LocalAddressBook(account, client);
String url = addrBook.getURL();
if (url != null) {
App.log.fine("Migrating address book " + url);
// insert CardDAV service
ContentValues values = new ContentValues();
values.put(Services.ACCOUNT_NAME, account.name);
values.put(Services.SERVICE, Services.SERVICE_CARDDAV);
long service = db.insert(Services._TABLE, null, values);
// insert address book
values.clear();
values.put(Collections.SERVICE_ID, service);
values.put(Collections.URL, url);
values.put(Collections.SYNC, 1);
db.insert(Collections._TABLE, null, values);
// insert home set
HttpUrl homeSet = HttpUrl.parse(url).resolve("../");
values.clear();
values.put(HomeSets.SERVICE_ID, service);
values.put(HomeSets.URL, homeSet.toString());
db.insert(HomeSets._TABLE, null, values);
}
} catch (ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't migrate address book", e);
} finally {
client.release();
}
// CalDAV: migrate calendars + task lists
Set<String> collections = new HashSet<>();
Set<HttpUrl> homeSets = new HashSet<>();
client = context.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
if (client != null)
try {
LocalCalendar calendars[] = (LocalCalendar[])LocalCalendar.find(account, client, LocalCalendar.Factory.INSTANCE, null, null);
for (LocalCalendar calendar : calendars) {
String url = calendar.getName();
App.log.fine("Migrating calendar " + url);
collections.add(url);
homeSets.add(HttpUrl.parse(url).resolve("../"));
}
} catch (CalendarStorageException e) {
App.log.log(Level.SEVERE, "Couldn't migrate calendars", e);
} finally {
client.release();
}
TaskProvider provider = LocalTaskList.acquireTaskProvider(context.getContentResolver());
if (provider != null)
try {
LocalTaskList[] taskLists = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
for (LocalTaskList taskList : taskLists) {
String url = taskList.getSyncId();
App.log.fine("Migrating task list " + url);
collections.add(url);
homeSets.add(HttpUrl.parse(url).resolve("../"));
}
} catch (CalendarStorageException e) {
App.log.log(Level.SEVERE, "Couldn't migrate task lists", e);
} finally {
provider.close();
}
if (!collections.isEmpty()) {
// insert CalDAV service
ContentValues values = new ContentValues();
values.put(Services.ACCOUNT_NAME, account.name);
values.put(Services.SERVICE, Services.SERVICE_CALDAV);
long service = db.insert(Services._TABLE, null, values);
// insert collections
for (String url : collections) {
values.clear();
values.put(Collections.SERVICE_ID, service);
values.put(Collections.URL, url);
values.put(Collections.SYNC, 1);
db.insert(Collections._TABLE, null, values);
}
// insert home sets
for (HttpUrl homeSet : homeSets) {
values.clear();
values.put(HomeSets.SERVICE_ID, service);
values.put(HomeSets.URL, homeSet.toString());
db.insert(HomeSets._TABLE, null, values);
}
}
} finally {
dbHelper.close();
}
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "3");
}
}

@ -8,6 +8,7 @@
package at.bitfire.davdroid;
import android.accounts.Account;
import android.app.Application;
import android.content.SharedPreferences;
import android.util.Log;
@ -16,6 +17,7 @@ import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
@ -31,8 +33,8 @@ import okhttp3.internal.tls.OkHostnameVerifier;
public class App extends Application implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String
PREF_FILE = "davdroid_preferences",
PREF_LOG_TO_FILE = "log_to_file";
PREF_FILE = "davdroid_preferences", // preference file name
PREF_LOG_TO_FILE = "log_to_file"; // boolean: external logging enabled
@Getter
private static MemorizingTrustManager memorizingTrustManager;
@ -60,6 +62,7 @@ public class App extends Application implements SharedPreferences.OnSharedPrefer
sslSocketFactoryCompat = new SSLSocketFactoryCompat(memorizingTrustManager);
hostnameVerifier = memorizingTrustManager.wrapHostnameVerifier(OkHostnameVerifier.INSTANCE);
// initializer logger
reinitLogger();
}

@ -43,9 +43,11 @@ public class PlainTextFormatter extends Formatter {
builder.append(Log.getStackTraceString(r.getThrown()));
}
if (r.getParameters() != null)
if (r.getParameters() != null) {
int idx = 1;
for (Object param : r.getParameters())
builder.append("\nPARAMETER " + param);
builder.append("\nPARAMETER #").append(idx).append(" = ").append(param);
}
if (!logcat)
builder.append("\n");

@ -127,7 +127,7 @@ public class CollectionInfo implements Serializable {
info.supportsVEVENT = booleanField(values, Collections.SUPPORTS_VEVENT);
info.supportsVTODO = booleanField(values, Collections.SUPPORTS_VTODO);
info.selected = booleanField(values, Collections.SELECTED);
info.selected = booleanField(values, Collections.SYNC);
return info;
}
@ -146,7 +146,7 @@ public class CollectionInfo implements Serializable {
if (supportsVTODO != null)
values.put(Collections.SUPPORTS_VTODO, supportsVTODO ? 1 : 0);
values.put(Collections.SELECTED, selected ? 1 : 0);
values.put(Collections.SYNC, selected ? 1 : 0);
return values;
}

@ -54,12 +54,12 @@ public class ServiceDB {
TIME_ZONE = "timezone",
SUPPORTS_VEVENT = "supportsVEVENT",
SUPPORTS_VTODO = "supportsVTODO",
SELECTED = "selected";
SYNC = "sync";
public static String[] _COLUMNS = new String[] {
ID, SERVICE_ID, URL, DISPLAY_NAME, DESCRIPTION, COLOR,
TIME_ZONE, SUPPORTS_VEVENT, SUPPORTS_VTODO,
SELECTED
SYNC
};
}
@ -110,7 +110,7 @@ public class ServiceDB {
Collections.TIME_ZONE + " TEXt NULL," +
Collections.SUPPORTS_VEVENT + " INTEGER NULL," +
Collections.SUPPORTS_VTODO + " INTEGER NULL," +
Collections.SELECTED + " INTEGER DEFAULT 0 NOT NULL" +
Collections.SYNC + " INTEGER DEFAULT 0 NOT NULL" +
")");
db.execSQL("CREATE UNIQUE INDEX collections_service_url ON " + Collections._TABLE + "(" + Collections.SERVICE_ID + "," + Collections.URL + ")");
}

@ -21,6 +21,9 @@ import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.model.CollectionInfo;
@ -65,25 +68,30 @@ public class ContactsSyncAdapterService extends Service {
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
long service = getService(account);
CollectionInfo remote = remoteAddressBook(service);
if (remote != null) {
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, authority, provider, syncResult, remote);
syncManager.performSync();
} else
App.log.info("No address book collection selected for synchronization");
Long service = getService(account);
if (service != null) {
CollectionInfo remote = remoteAddressBook(service);
if (remote != null) {
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, authority, provider, syncResult, remote);
syncManager.performSync();
} else
App.log.info("No address book collection selected for synchronization");
}
App.log.info("Address book sync complete");
}
private long getService(@NonNull Account account) {
@Nullable
private Long getService(@NonNull Account account) {
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
c.moveToNext();
return c.getLong(0);
if (c.moveToNext())
return c.getLong(0);
else
return null;
}
@Nullable
private CollectionInfo remoteAddressBook(long service) {
@Cleanup Cursor c = db.query(Collections._TABLE, Collections._COLUMNS,
Collections.SERVICE_ID + "=? AND selected", new String[] { String.valueOf(service) }, null, null, null);

@ -62,7 +62,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavService;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
@ -199,12 +201,12 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
if (list.getChoiceMode() == AbsListView.CHOICE_MODE_SINGLE) {
// disable all other collections
ContentValues values = new ContentValues(1);
values.put(Collections.SELECTED, 0);
values.put(Collections.SYNC, 0);
db.update(Collections._TABLE, values, Collections.SERVICE_ID + "=?", new String[] { String.valueOf(info.serviceID) });
}
ContentValues values = new ContentValues(1);
values.put(Collections.SELECTED, nowChecked ? 1 : 0);
values.put(Collections.SYNC, nowChecked ? 1 : 0);
db.update(Collections._TABLE, values, Collections.ID + "=?", new String[] { String.valueOf(info.id) });
db.setTransactionSuccessful();
@ -314,8 +316,11 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public void onLoaderReset(Loader<AccountInfo> loader) {
listCardDAV.setAdapter(null);
listCalDAV.setAdapter(null);
if (listCardDAV != null)
listCardDAV.setAdapter(null);
if (listCalDAV != null)
listCalDAV.setAdapter(null);
}
@ -361,6 +366,11 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public AccountInfo loadInBackground() {
// peek into AccountSettings to call possible 0.9 -> 1.0 migration
// The next line can be removed as soon as migration from 0.9 is not required anymore!
new AccountSettings(getContext(), new Account(accountName, Constants.ACCOUNT_TYPE));
// get account info
AccountInfo info = new AccountInfo();
try {
SQLiteDatabase db = dbHelper.getReadableDatabase();

@ -27,6 +27,7 @@ import java.util.List;
import java.util.logging.Level;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalTaskList;
@ -39,24 +40,28 @@ public class StartupDialogFragment extends DialogFragment {
private static final String ARGS_MODE = "mode";
enum Mode {
DEVELOPMENT_VERSION,
FDROID_DONATE,
GOOGLE_PLAY_ACCOUNTS_REMOVED,
OPENTASKS_NOT_INSTALLED
}
public static StartupDialogFragment[] getStartupDialogs(Context context) {
List<StartupDialogFragment> dialogs = new LinkedList<>();
// store-specific information
final String installedFrom = installedFrom(context);
if (installedFrom == null || installedFrom.startsWith("org.fdroid"))
dialogs.add(StartupDialogFragment.instantiate(Mode.FDROID_DONATE));
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && // only on Android <5
"com.android.vending".equals(installedFrom) && // only when installed from Play Store
App.getPreferences().getBoolean(PREF_HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, true)) // and only when "Don't show again" hasn't been clicked yet
dialogs.add(StartupDialogFragment.instantiate(Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED));
if (BuildConfig.VERSION_NAME.contains("-alpha") || BuildConfig.VERSION_NAME.contains("-beta"))
dialogs.add(StartupDialogFragment.instantiate(Mode.DEVELOPMENT_VERSION));
else {
// store-specific information
final String installedFrom = installedFrom(context);
if (installedFrom == null || installedFrom.startsWith("org.fdroid"))
dialogs.add(StartupDialogFragment.instantiate(Mode.FDROID_DONATE));
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && // only on Android <5
"com.android.vending".equals(installedFrom) && // only when installed from Play Store
App.getPreferences().getBoolean(PREF_HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, true)) // and only when "Don't show again" hasn't been clicked yet
dialogs.add(StartupDialogFragment.instantiate(Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED));
}
// OpenTasks information
if (!LocalTaskList.tasksProviderAvailable(context.getContentResolver()) &&
@ -82,6 +87,24 @@ public class StartupDialogFragment extends DialogFragment {
Mode mode = Mode.valueOf(getArguments().getString(ARGS_MODE));
switch (mode) {
case DEVELOPMENT_VERSION:
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)
.setTitle(R.string.startup_development_version)
.setMessage(R.string.startup_development_version_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setNeutralButton(R.string.startup_development_version_show_forums, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/").build()));
}
})
.create();
case FDROID_DONATE:
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)

@ -19,6 +19,10 @@
<!-- startup dialogs -->
<string name="startup_dont_show_again">Don\'t show again</string>
<string name="startup_development_version">DAVdroid Preview Release</string>
<string name="startup_development_version_message">This is a development version of DAVdroid. Be aware that things
may not work as expected. Please give us constructive feedback to improve DAVdroid.</string>
<string name="startup_development_version_show_forums">Show forums</string>
<string name="startup_donate">Open-Source Information</string>
<string name="startup_donate_message">We\'re happy that you use DAVdroid, which is open-source software (GPLv3). Because developing DAVdroid is hard work and took us thousands of working hours, please consider a donation.</string>
<string name="startup_donate_now">Show donation page</string>
@ -147,6 +151,8 @@
<item quantity="other">Events more than %d days in the past will be ignored</item>
</plurals>
<string name="settings_sync_time_range_past_message">Events which are more than this number of days in the past (may be 0) will be ignored. Leave blank to synchronize all events.</string>
<string name="settings_version_update">DAVdroid version update</string>
<string name="settings_version_update_warning">Internal settings have been updated. In case of problems, please uninstall DAVdroid and then install it again.</string>
<!-- collection management -->
<string name="create_addressbook">Create address book</string>
@ -175,11 +181,6 @@
<string name="exception_ioexception">An I/O error has occurred.</string>
<string name="exception_show_details">Show details</string>
<string name="settings_android_update_title">Android version update</string>
<string name="settings_android_update_description">Android version updates may have an impact on how DAVdroid works. If there are problems, please delete your DAVdroid accounts and add them again.</string>
<string name="settings_version_update_title">Settings have been updated</string>
<string name="settings_version_update_description">Internal settings have been updated. If there are problems, please uninstall DAVdroid and then install it again.</string>
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Debug info</string>
<string name="sync_error_calendar">Calendar synchronization failed (%s)</string>

@ -1 +1 @@
Subproject commit 5e7334cea2bbaacde7f61b9806c907ba7404753d
Subproject commit 0532282932b71209000e158dd9e6a1725c58b117
Loading…
Cancel
Save