Cloud sync: removed account sync concept

This commit is contained in:
M66B 2023-01-17 23:05:56 +01:00
parent 2a9c81417d
commit f7d0f99342
8 changed files with 6 additions and 3614 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,8 @@ package eu.faircode.email;
Copyright 2018-2023 by Marcel Bokhorst (M66B)
*/
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
@ -47,10 +44,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@ -84,12 +79,6 @@ public class CloudSync {
long lrevision = prefs.getLong("sync_status", new Date().getTime());
Log.i("Cloud local revision=" + lrevision + " (" + new Date(lrevision) + ")");
Long lastUpdate = getLastUpdate(context);
Log.i("Cloud last update=" + (lastUpdate == null ? null : new Date(lastUpdate)));
if (lastUpdate != null && lrevision > lastUpdate)
Log.w("Cloud invalid local revision" +
" lrevision=" + lrevision + " last=" + lastUpdate);
JSONObject jsyncstatus = new JSONObject();
jsyncstatus.put("key", "sync.status");
jsyncstatus.put("rev", lrevision);
@ -104,7 +93,6 @@ public class CloudSync {
if (jitems.length() == 0) {
Log.i("Cloud server is empty");
sendLocalData(context, user, password, lrevision);
} else if (jitems.length() == 1) {
Log.i("Cloud sync check");
jsyncstatus = jitems.getJSONObject(0);
@ -113,33 +101,9 @@ public class CloudSync {
int sync_version = jstatus.optInt("sync.version", 0);
int app_version = jstatus.optInt("app.version", 0);
Log.i("Cloud version sync=" + sync_version + " app=" + app_version +
" local=" + lrevision + " last=" + lastUpdate + " remote=" + rrevision);
// last > local (local mods) && remote > local (remote mods) = CONFLICT
// local > last = ignorable ERROR
// remote > local = fetch remote
// last > remote = send local
if (lastUpdate != null && lastUpdate > rrevision) // local newer than remote
sendLocalData(context, user, password, lastUpdate);
else if (rrevision > lrevision) // remote changes
if (lastUpdate != null && lastUpdate > lrevision) { // local changes
Log.w("Cloud conflict" +
" lrevision=" + lrevision + " last=" + lastUpdate + " rrevision=" + rrevision);
if (manual)
if (lastUpdate >= rrevision)
sendLocalData(context, user, password, lastUpdate);
else
receiveRemoteData(context, user, password, lrevision, jstatus);
} else
receiveRemoteData(context, user, password, lrevision, jstatus);
else if (BuildConfig.DEBUG)
receiveRemoteData(context, user, password, lrevision - 1, jstatus);
" local=" + lrevision + " remote=" + rrevision);
} else
throw new IllegalArgumentException("Expected one status item");
if (lastUpdate != null)
db.sync().deleteSyncByTime(lastUpdate);
} else {
JSONArray jitems = new JSONArray();
jrequest.put("items", jitems);
@ -149,411 +113,6 @@ public class CloudSync {
prefs.edit().putLong("cloud_last_sync", new Date().getTime()).apply();
}
private static Long getLastUpdate(Context context) {
DB db = DB.getInstance(context);
Long lastUpdate = null;
for (EntitySync sync : db.sync().getSync(null, null, Long.MAX_VALUE)) {
Log.i("Cloud sync " + sync.entity + ":" + sync.reference + " " + sync.action + " " + new Date(sync.time));
if (sync.reference == null) {
Log.w("Cloud reference missing");
db.sync().deleteSync(sync.id);
continue;
}
if ("account".equals(sync.entity) && "auth".equals(sync.action)) {
EntityAccount account = db.account().getAccountByUUID(sync.reference);
if (account == null || account.auth_type != AUTH_TYPE_PASSWORD) {
if (account == null)
Log.w("Cloud account missing uuid=" + sync.reference);
else
Log.i("Cloud account oauth uuid=" + sync.reference);
db.sync().deleteSync(sync.id);
continue;
}
}
if ("identity".equals(sync.entity) && "auth".equals(sync.action)) {
EntityIdentity identity = db.identity().getIdentityByUUID(sync.reference);
if (identity == null || identity.auth_type != AUTH_TYPE_PASSWORD) {
if (identity == null)
Log.w("Cloud identity missing uuid=" + sync.reference);
else
Log.i("Cloud identity oauth uuid=" + sync.reference);
db.sync().deleteSync(sync.id);
continue;
}
}
if (lastUpdate == null || sync.time > lastUpdate)
lastUpdate = sync.time;
}
return lastUpdate;
}
private static void sendLocalData(Context context, String user, String password, long lrevision)
throws JSONException, GeneralSecurityException, IOException {
DB db = DB.getInstance(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<EntityAccount> accounts = db.account().getSynchronizingAccounts(null);
Log.i("Cloud accounts=" + (accounts == null ? null : accounts.size()));
if (accounts == null || accounts.size() == 0) {
Log.i("Cloud no accounts");
return;
}
JSONArray jupload = new JSONArray();
JSONArray jaccountuuidlist = new JSONArray();
for (EntityAccount account : accounts)
if (!TextUtils.isEmpty(account.uuid)) {
jaccountuuidlist.put(account.uuid);
JSONArray jidentitieuuids = new JSONArray();
List<EntityIdentity> identities = db.identity().getIdentities(account.id);
if (identities != null)
for (EntityIdentity identity : identities)
if (!TextUtils.isEmpty(identity.uuid)) {
jidentitieuuids.put(identity.uuid);
JSONObject jidentity = new JSONObject();
jidentity.put("key", "identity." + identity.uuid);
jidentity.put("val", toJSON(identity).toString());
jidentity.put("rev", lrevision);
jupload.put(jidentity);
}
JSONObject jaccountdata = new JSONObject();
jaccountdata.put("account", toJSON(account));
jaccountdata.put("identities", jidentitieuuids);
JSONObject jaccount = new JSONObject();
jaccount.put("key", "account." + account.uuid);
jaccount.put("val", jaccountdata.toString());
jaccount.put("rev", lrevision);
jupload.put(jaccount);
}
JSONObject jaccountuuids = new JSONObject();
jaccountuuids.put("uuids", jaccountuuidlist);
JSONObject jstatus = new JSONObject();
jstatus.put("sync.version", 1);
jstatus.put("app.version", BuildConfig.VERSION_CODE);
jstatus.put("accounts", jaccountuuids);
JSONObject jsyncstatus = new JSONObject();
jsyncstatus.put("key", "sync.status");
jsyncstatus.put("val", jstatus.toString());
jsyncstatus.put("rev", lrevision);
jupload.put(jsyncstatus);
JSONObject jrequest = new JSONObject();
jrequest.put("items", jupload);
call(context, user, password, "write", jrequest);
prefs.edit().putLong("sync_status", lrevision).apply();
}
private static void receiveRemoteData(Context context, String user, String password, long lrevision, JSONObject jstatus)
throws JSONException, GeneralSecurityException, IOException {
DB db = DB.getInstance(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean sync_accounts = prefs.getBoolean("cloud_sync_accounts", true);
boolean sync_accounts_delete = prefs.getBoolean("cloud_sync_accounts_delete", false);
boolean sync_blocked_senders = prefs.getBoolean("cloud_sync_blocked_senders", true);
boolean sync_filter_rules = prefs.getBoolean("cloud_sync_filter_rules", true);
// New revision
boolean updates = false;
JSONArray jdownload = new JSONArray();
// Get accounts
JSONObject jaccountstatus = jstatus.getJSONObject("accounts");
JSONArray jaccountuuidlist = jaccountstatus.getJSONArray("uuids");
for (int i = 0; i < jaccountuuidlist.length(); i++) {
String uuid = jaccountuuidlist.getString(i);
JSONObject jaccount = new JSONObject();
jaccount.put("key", "account." + uuid);
jaccount.put("rev", lrevision);
jdownload.put(jaccount);
Log.i("Cloud account uuid=" + uuid);
}
if (jdownload.length() > 0) {
Log.i("Cloud getting accounts");
JSONObject jrequest = new JSONObject();
jrequest.put("items", jdownload);
JSONObject jresponse = call(context, user, password, "sync", jrequest);
// Process accounts
Log.i("Cloud processing accounts");
JSONArray jitems = jresponse.getJSONArray("items");
jdownload = new JSONArray();
for (int i = 0; i < jitems.length(); i++) {
JSONObject jaccount = jitems.getJSONObject(i);
String value = jaccount.getString("val");
long revision = jaccount.getLong("rev");
JSONObject jaccountdata = new JSONObject(value);
EntityAccount raccount = accountFromJSON(jaccountdata.getJSONObject("account"));
EntityAccount laccount = db.account().getAccountByUUID(raccount.uuid);
JSONArray jidentities = jaccountdata.getJSONArray("identities");
Log.i("Cloud account " + raccount.uuid + "=" +
(laccount == null ? "insert" : (areEqual(toJSON(raccount), toJSON(laccount)) ? "equal" : "update")) +
" rev=" + revision +
" identities=" + jidentities +
" size=" + value.length());
for (int j = 0; j < jidentities.length(); j++) {
JSONObject jidentity = new JSONObject();
jidentity.put("key", "identity." + jidentities.getString(j));
jidentity.put("rev", lrevision);
jdownload.put(jidentity);
}
}
if (jdownload.length() > 0) {
// Get identities
Log.i("Cloud getting identities");
jrequest.put("items", jdownload);
jresponse = call(context, user, password, "sync", jrequest);
// Process identities
Log.i("Cloud processing identities");
jitems = jresponse.getJSONArray("items");
for (int i = 0; i < jitems.length(); i++) {
JSONObject jidentity = jitems.getJSONObject(i);
String value = jidentity.getString("val");
long revision = jidentity.getLong("rev");
EntityIdentity ridentity = identityFromJSON(new JSONObject(value));
EntityIdentity lidentity = db.identity().getIdentityByUUID(ridentity.uuid);
Log.i("Cloud identity " + ridentity.uuid + "=" +
(lidentity == null ? "insert" : (areEqual(toJSON(ridentity), toJSON(lidentity)) ? "equal" : "update")) +
" rev=" + revision +
" size=" + value.length());
}
}
}
prefs.edit().putLong("sync_status", lrevision).apply();
if (updates)
ServiceSynchronize.reload(context, null, true, "sync");
}
private static boolean areEqual(JSONObject o1, JSONObject o2) throws JSONException {
if (o1 == null && o2 == null)
return true;
if (o1 == null || o2 == null)
return false;
Iterator<String> i1 = o1.keys();
while (i1.hasNext()) {
String k1 = i1.next();
if (!o2.has(k1))
return false;
}
Iterator<String> i2 = o2.keys();
while (i2.hasNext()) {
String k2 = i2.next();
if (!o2.has(k2))
return false;
if (!Objects.equals(o1.get(k2), o2.get(k2)))
return false;
}
return true;
}
private static JSONObject toJSON(EntityAccount account) throws JSONException {
JSONObject json = new JSONObject();
if (account == null)
return json;
//json.put("id", id);
json.put("uuid", account.uuid);
//json.put("order", order);
json.put("protocol", account.protocol);
json.put("host", account.host);
json.put("encryption", account.encryption);
json.put("insecure", account.insecure);
json.put("port", account.port);
json.put("auth_type", account.auth_type);
json.put("provider", account.provider);
json.put("user", account.user);
json.put("password", account.password);
//json.put("certificate_alias", certificate_alias);
json.put("realm", account.realm);
json.put("fingerprint", account.fingerprint);
//json.put("name", name);
//json.put("category", category);
//json.put("color", color);
//json.put("calendar", calendar);
//json.put("synchronize", synchronize);
//json.put("ondemand", ondemand);
//json.put("poll_exempted", poll_exempted);
//json.put("primary", primary);
//json.put("notify", notify);
//json.put("browse", browse);
//json.put("leave_on_server", leave_on_server);
//json.put("leave_deleted", leave_deleted);
//json.put("leave_on_device", leave_on_device);
//json.put("max_messages", max_messages);
//json.put("auto_seen", auto_seen);
// not separator
//json.put("swipe_left", swipe_left);
//json.put("swipe_right", swipe_right);
//json.put("move_to", move_to);
json.put("poll_interval", account.poll_interval);
json.put("keep_alive_noop", account.keep_alive_noop);
json.put("partial_fetch", account.partial_fetch);
json.put("ignore_size", account.ignore_size);
json.put("use_date", account.use_date);
json.put("use_received", account.use_received);
json.put("unicode", account.unicode);
//json.put("conditions", conditions);
// not prefix
// not created
// not tbd
// not state
// not warning
// not error
// not last connected
return json;
}
public static EntityAccount accountFromJSON(JSONObject json) throws JSONException {
EntityAccount account = new EntityAccount();
account.uuid = json.getString("uuid");
if (json.has("protocol"))
account.protocol = json.getInt("protocol");
account.host = json.getString("host");
account.encryption = json.getInt("encryption");
account.insecure = (json.has("insecure") && json.getBoolean("insecure"));
account.port = json.getInt("port");
account.auth_type = json.getInt("auth_type");
if (json.has("provider") && !json.isNull("provider"))
account.provider = json.getString("provider");
account.user = json.getString("user");
account.password = json.getString("password");
if (json.has("realm") && !json.isNull("realm"))
account.realm = json.getString("realm");
if (json.has("fingerprint") && !json.isNull("fingerprint"))
account.fingerprint = json.getString("fingerprint");
account.poll_interval = json.getInt("poll_interval");
account.keep_alive_noop = json.optBoolean("keep_alive_noop");
account.partial_fetch = json.optBoolean("partial_fetch", true);
account.ignore_size = json.optBoolean("ignore_size", false);
account.use_date = json.optBoolean("use_date", false);
account.use_received = json.optBoolean("use_received", false);
account.unicode = json.optBoolean("unicode", false);
return account;
}
private static JSONObject toJSON(EntityIdentity identity) throws JSONException {
JSONObject json = new JSONObject();
if (identity == null)
return json;
//json.put("id", id);
json.put("uuid", identity.uuid);
//json.put("name", name);
json.put("email", identity.email);
// not account
//json.put("display", display);
//if (color != null)
// json.put("color", color);
//TODO json.put("signature", signature);
json.put("host", identity.host);
json.put("encryption", identity.encryption);
json.put("insecure", identity.insecure);
json.put("port", identity.port);
json.put("auth_type", identity.auth_type);
json.put("provider", identity.provider);
json.put("user", identity.user);
json.put("password", identity.password);
//json.put("certificate_alias", certificate_alias);
json.put("realm", identity.realm);
json.put("fingerprint", identity.fingerprint);
json.put("use_ip", identity.use_ip);
json.put("ehlo", identity.ehlo);
//json.put("synchronize", synchronize);
//json.put("primary", primary);
//TODO json.put("self", self);
//TODO json.put("sender_extra", sender_extra);
//TODO json.put("sender_extra_name", sender_extra_name);
//TODO json.put("sender_extra_regex", sender_extra_regex);
//json.put("replyto", replyto);
//json.put("cc", cc);
//json.put("bcc", bcc);
//json.put("internal", internal);
json.put("unicode", identity.unicode);
json.put("octetmime", identity.octetmime);
// not plain_only
//json.put("sign_default", sign_default);
//json.put("encrypt_default", encrypt_default);
// not encrypt
// delivery_receipt
// read_receipt
// not store_sent
// not sent_folder
// not sign_key
// sign_key_alias
// not tbd
// not state
// not error
// not last_connected
// not max_size
return json;
}
public static EntityIdentity identityFromJSON(JSONObject json) throws JSONException {
EntityIdentity identity = new EntityIdentity();
identity.uuid = json.getString("uuid");
identity.email = json.getString("email");
identity.host = json.getString("host");
identity.encryption = json.getInt("encryption");
identity.insecure = json.optBoolean("insecure");
identity.port = json.getInt("port");
identity.auth_type = json.getInt("auth_type");
if (json.has("provider") && !json.isNull("provider"))
identity.provider = json.getString("provider");
identity.user = json.getString("user");
identity.password = json.getString("password");
if (json.has("realm") && !json.isNull("realm"))
identity.realm = json.getString("realm");
if (json.has("fingerprint") && !json.isNull("fingerprint"))
identity.fingerprint = json.getString("fingerprint");
if (json.has("use_ip"))
identity.use_ip = json.getBoolean("use_ip");
if (json.has("ehlo") && !json.isNull("ehlo"))
identity.ehlo = json.getString("ehlo");
identity.unicode = json.optBoolean("unicode");
identity.octetmime = json.optBoolean("octetmime");
return identity;
}
// Lower level
public static JSONObject call(Context context, String user, String password, String command, JSONObject jrequest)

View File

@ -68,7 +68,7 @@ import javax.mail.internet.InternetAddress;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 262,
version = 261,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -81,8 +81,7 @@ import javax.mail.internet.InternetAddress;
EntityAnswer.class,
EntityRule.class,
EntitySearch.class,
EntityLog.class,
EntitySync.class
EntityLog.class
},
views = {
TupleAccountView.class,
@ -117,8 +116,6 @@ public abstract class DB extends RoomDatabase {
public abstract DaoLog log();
public abstract DaoSync sync();
private static int sPid;
private static Context sContext;
private static DB sInstance;
@ -479,19 +476,9 @@ public abstract class DB extends RoomDatabase {
Log.i("Get PRAGMA " + pragma + "=<?>");
}
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG && false) {
db.execSQL("DROP TRIGGER IF EXISTS `attachment_insert`");
db.execSQL("DROP TRIGGER IF EXISTS `attachment_delete`");
db.execSQL("DROP TRIGGER IF EXISTS `account_insert`");
db.execSQL("DROP TRIGGER IF EXISTS `account_update`");
db.execSQL("DROP TRIGGER IF EXISTS `account_auth`");
db.execSQL("DROP TRIGGER IF EXISTS `account_delete`");
db.execSQL("DROP TRIGGER IF EXISTS `identity_insert`");
db.execSQL("DROP TRIGGER IF EXISTS `identity_update`");
db.execSQL("DROP TRIGGER IF EXISTS `identity_auth`");
db.execSQL("DROP TRIGGER IF EXISTS `identity_delete`");
}
createTriggers(db);
@ -553,72 +540,6 @@ public abstract class DB extends RoomDatabase {
" AND OLD.encryption IS NULL" +
" AND NOT ((OLD.disposition = 'inline' OR (OLD.related IS NOT 0 AND OLD.cid IS NOT NULL)) AND OLD.type IN (" + images + "));" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_insert" +
" AFTER INSERT ON account" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('account', NEW.uuid, 'insert', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_update" +
" AFTER UPDATE" +
" OF host, encryption, insecure, port, realm, fingerprint" +
", poll_interval, keep_alive_noop, partial_fetch, ignore_size, use_date, use_received, unicode" +
" ON account" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('account', NEW.uuid, 'update', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_auth" +
" AFTER UPDATE" +
" OF auth_type, provider, `user`, password" +
" ON account" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('account', NEW.uuid, 'auth', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_delete" +
" AFTER DELETE ON account" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('account', OLD.uuid, 'delete', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_insert" +
" AFTER INSERT ON identity" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('identity', NEW.uuid, 'insert', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_update" +
" AFTER UPDATE" +
" OF email, host, encryption, insecure, port, realm, fingerprint" +
", use_ip, ehlo, unicode, octetmime" +
" ON identity" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('identity', NEW.uuid, 'update', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_auth" +
" AFTER UPDATE" +
" OF auth_type, provider, `user`, password" +
" ON identity" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('identity', NEW.uuid, 'auth', strftime('%s') * 1000);" +
" END");
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_delete" +
" AFTER DELETE ON identity" +
" BEGIN" +
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
" VALUES ('identity', OLD.uuid, 'delete', strftime('%s') * 1000);" +
" END");
}
private static void logMigration(int startVersion, int endVersion) {
@ -2716,20 +2637,6 @@ public abstract class DB extends RoomDatabase {
else
db.execSQL("UPDATE `folder` SET `hide_seen` = 0");
}
}).addMigrations(new Migration(261, 262) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
logMigration(startVersion, endVersion);
db.execSQL("CREATE TABLE `sync`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
", `entity` TEXT NOT NULL" +
", `reference` TEXT" +
", `action` TEXT NOT NULL" +
", `time` INTEGER NOT NULL)");
db.execSQL("CREATE INDEX IF NOT EXISTS `index_sync_entity_reference` ON `sync` (`entity`, `reference`)");
db.execSQL("CREATE INDEX IF NOT EXISTS `index_sync_time` ON `sync` (`time`)");
createTriggers(db);
}
}).addMigrations(new Migration(998, 999) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {

View File

@ -1,41 +0,0 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2023 by Marcel Bokhorst (M66B)
*/
import androidx.room.Dao;
import androidx.room.Query;
import java.util.List;
@Dao
public interface DaoSync {
@Query("SELECT * FROM sync" +
" WHERE (:entity IS NULL OR entity = :entity)" +
" AND (:reference IS NULL OR reference = :reference)" +
" AND time <= :time" +
" ORDER BY time")
List<EntitySync> getSync(String entity, String reference, long time);
@Query("DELETE FROM sync WHERE id= :id")
int deleteSync(long id);
@Query("DELETE FROM sync WHERE time <= :time")
int deleteSyncByTime(long time);
}

View File

@ -1,49 +0,0 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2023 by Marcel Bokhorst (M66B)
*/
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Index;
import androidx.room.PrimaryKey;
@Entity(
tableName = EntitySync.TABLE_NAME,
foreignKeys = {
},
indices = {
@Index(value = {"entity", "reference"}),
@Index(value = {"time"})
}
)
public class EntitySync {
static final String TABLE_NAME = "sync";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public String entity;
public String reference;
@NonNull
public String action;
@NonNull
public Long time;
}

View File

@ -118,10 +118,7 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
private TextInputLayout tilPassword;
private Button btnLogin;
private TextView tvLogin;
private CheckBox cbAccounts;
private CheckBox cbAccountsDelete;
private CheckBox cbBlockedSenders;
private CheckBox cbFilterRules;
private ImageButton ibSync;
private TextView tvLastSync;
private Button btnLogout;
@ -162,10 +159,7 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
tilPassword = view.findViewById(R.id.tilPassword);
btnLogin = view.findViewById(R.id.btnLogin);
tvLogin = view.findViewById(R.id.tvLogin);
cbAccounts = view.findViewById(R.id.cbAccounts);
cbAccountsDelete = view.findViewById(R.id.cbAccountsDelete);
cbBlockedSenders = view.findViewById(R.id.cbBlockedSenders);
cbFilterRules = view.findViewById(R.id.cbFilterRules);
ibSync = view.findViewById(R.id.ibSync);
tvLastSync = view.findViewById(R.id.tvLastSync);
btnLogout = view.findViewById(R.id.btnLogout);
@ -212,21 +206,6 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
}
});
cbAccounts.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
prefs.edit().putBoolean("cloud_sync_accounts", isChecked).apply();
cbAccountsDelete.setEnabled(isChecked);
}
});
cbAccountsDelete.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
prefs.edit().putBoolean("cloud_sync_accounts_delete", isChecked).apply();
}
});
cbBlockedSenders.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -234,13 +213,6 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
}
});
cbFilterRules.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
prefs.edit().putBoolean("cloud_sync_filter_rules", isChecked).apply();
}
});
ibSync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -266,11 +238,7 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
Helper.linkPro(tvCloudPro);
prefs.registerOnSharedPreferenceChangeListener(this);
cbAccounts.setChecked(prefs.getBoolean("cloud_sync_accounts", true));
cbAccountsDelete.setChecked(prefs.getBoolean("cloud_sync_accounts_delete", false));
cbAccountsDelete.setEnabled(cbAccounts.isChecked());
cbBlockedSenders.setChecked(prefs.getBoolean("cloud_sync_blocked_senders", true));
cbFilterRules.setChecked(prefs.getBoolean("cloud_sync_filter_rules", true));
onSharedPreferenceChanged(prefs, null);
return view;

View File

@ -303,38 +303,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvRegister" />
<CheckBox
android:id="@+id/cbAccounts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_cloud_accounts"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvLogin" />
<CheckBox
android:id="@+id/cbAccountsDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_cloud_accounts_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbAccounts" />
<TextView
android:id="@+id/tvAccountsInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:text="@string/title_advanced_cloud_accounts_info"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbAccountsDelete" />
<CheckBox
android:id="@+id/cbBlockedSenders"
android:layout_width="wrap_content"
@ -342,16 +310,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_advanced_cloud_blocked_senders"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccountsInfo" />
<CheckBox
android:id="@+id/cbFilterRules"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_cloud_filter_rules"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbBlockedSenders" />
app:layout_constraintTop_toBottomOf="@id/tvLogin" />
<ImageButton
android:id="@+id/ibSync"
@ -359,7 +318,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbFilterRules"
app:layout_constraintTop_toBottomOf="@id/cbBlockedSenders"
app:srcCompat="@drawable/twotone_compare_arrows_24" />
<TextView

View File

@ -979,11 +979,7 @@
<string name="title_advanced_cloud_login" translatable="false">Login</string>
<string name="title_advanced_cloud_register" translatable="false">Logging in for the first time will automatically create an account</string>
<string name="title_advanced_cloud_invalid" translatable="false">Invalid username or password</string>
<string name="title_advanced_cloud_accounts" translatable="false">Sync accounts</string>
<string name="title_advanced_cloud_accounts_delete" translatable="false">Delete accounts</string>
<string name="title_advanced_cloud_accounts_info" translatable="false">Only the server configuration will be synchronized</string>
<string name="title_advanced_cloud_blocked_senders" translatable="false">Sync blocked senders</string>
<string name="title_advanced_cloud_filter_rules" translatable="false">Sync filter rules</string>
<string name="title_advanced_cloud_last_sync" translatable="false">Last sync: %1$s</string>
<string name="title_advanced_cloud_update" translatable="false">Update settings</string>
<string name="title_advanced_cloud_logout" translatable="false">Logout</string>