Fixed synchronization

This commit is contained in:
M66B 2018-12-03 08:56:34 +01:00
parent 3dced44622
commit 22628aef78
1 changed files with 214 additions and 209 deletions

View File

@ -912,114 +912,29 @@ public class ServiceSynchronize extends LifecycleService {
ifolder.addMessageCountListener(new MessageCountAdapter() { ifolder.addMessageCountListener(new MessageCountAdapter() {
@Override @Override
public void messagesAdded(MessageCountEvent e) { public void messagesAdded(MessageCountEvent e) {
synchronized (folder) { try {
try { wlAccount.acquire();
wlAccount.acquire(); Log.i(Helper.TAG, folder.name + " messages added");
Log.i(Helper.TAG, folder.name + " messages added");
FetchProfile fp = new FetchProfile(); FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.FLAGS); fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.CONTENT_INFO); // body structure fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
fp.add(UIDFolder.FetchProfileItem.UID); fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.HEADERS); fp.add(IMAPFolder.FetchProfileItem.HEADERS);
fp.add(IMAPFolder.FetchProfileItem.MESSAGE); fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
fp.add(FetchProfile.Item.SIZE); fp.add(FetchProfile.Item.SIZE);
fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
ifolder.fetch(e.getMessages(), fp); ifolder.fetch(e.getMessages(), fp);
for (Message imessage : e.getMessages()) for (Message imessage : e.getMessages())
try {
long id;
try {
db.beginTransaction();
id = synchronizeMessage(
ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) imessage,
false, false, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
try {
db.beginTransaction();
downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (IOException ex) {
if (ex.getCause() instanceof MessageRemovedException)
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
else
throw ex;
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wlAccount.release();
}
}
}
@Override
public void messagesRemoved(MessageCountEvent e) {
synchronized (folder) {
try {
wlAccount.acquire();
Log.i(Helper.TAG, folder.name + " messages removed");
for (Message imessage : e.getMessages())
try {
long uid = ifolder.getUID(imessage);
DB db = DB.getInstance(ServiceSynchronize.this);
int count = db.message().deleteMessage(folder.id, uid);
Log.i(Helper.TAG, "Deleted uid=" + uid + " count=" + count);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wlAccount.release();
}
}
}
});
// Flags (like "seen") at the remote could be changed while synchronizing
// Listen for changed messages
ifolder.addMessageChangedListener(new MessageChangedListener() {
@Override
public void messageChanged(MessageChangedEvent e) {
synchronized (folder) {
try {
wlAccount.acquire();
try { try {
Log.i(Helper.TAG, folder.name + " message changed");
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
ifolder.fetch(new Message[]{e.getMessage()}, fp);
long id; long id;
try { try {
db.beginTransaction(); db.beginTransaction();
id = synchronizeMessage( id = synchronizeMessage(
ServiceSynchronize.this, ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) e.getMessage(), folder, ifolder, (IMAPMessage) imessage,
false, false, false); false, false, false);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
@ -1028,7 +943,7 @@ public class ServiceSynchronize extends LifecycleService {
try { try {
db.beginTransaction(); db.beginTransaction();
downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), id); downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();
@ -1041,14 +956,93 @@ public class ServiceSynchronize extends LifecycleService {
else else
throw ex; throw ex;
} }
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex); reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex)); db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error(); state.error();
} finally { } finally {
wlAccount.release(); wlAccount.release();
}
}
@Override
public void messagesRemoved(MessageCountEvent e) {
try {
wlAccount.acquire();
Log.i(Helper.TAG, folder.name + " messages removed");
for (Message imessage : e.getMessages())
try {
long uid = ifolder.getUID(imessage);
DB db = DB.getInstance(ServiceSynchronize.this);
int count = db.message().deleteMessage(folder.id, uid);
Log.i(Helper.TAG, "Deleted uid=" + uid + " count=" + count);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wlAccount.release();
}
}
});
// Flags (like "seen") at the remote could be changed while synchronizing
// Listen for changed messages
ifolder.addMessageChangedListener(new MessageChangedListener() {
@Override
public void messageChanged(MessageChangedEvent e) {
try {
wlAccount.acquire();
try {
Log.i(Helper.TAG, folder.name + " message changed");
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
ifolder.fetch(new Message[]{e.getMessage()}, fp);
long id;
try {
db.beginTransaction();
id = synchronizeMessage(
ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) e.getMessage(),
false, false, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
try {
db.beginTransaction();
downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), id);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (IOException ex) {
if (ex.getCause() instanceof MessageRemovedException)
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
else
throw ex;
} }
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wlAccount.release();
} }
} }
}); });
@ -1332,126 +1326,124 @@ public class ServiceSynchronize extends LifecycleService {
} }
private void processOperations(EntityAccount account, EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, ServiceState state) throws MessagingException, JSONException, IOException { private void processOperations(EntityAccount account, EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, ServiceState state) throws MessagingException, JSONException, IOException {
synchronized (folder) { try {
try { Log.i(Helper.TAG, folder.name + " start process");
Log.i(Helper.TAG, folder.name + " start process");
DB db = DB.getInstance(this);
List<EntityOperation> ops = db.operation().getOperationsByFolder(folder.id);
Log.i(Helper.TAG, folder.name + " pending operations=" + ops.size());
for (int i = 0; i < ops.size() && state.running(); i++) {
EntityOperation op = ops.get(i);
try {
Log.i(Helper.TAG, folder.name +
" start op=" + op.id + "/" + op.name +
" msg=" + op.message +
" args=" + op.args);
// Fetch most recent copy of message
EntityMessage message = null;
if (op.message != null)
message = db.message().getMessage(op.message);
DB db = DB.getInstance(this);
List<EntityOperation> ops = db.operation().getOperationsByFolder(folder.id);
Log.i(Helper.TAG, folder.name + " pending operations=" + ops.size());
for (int i = 0; i < ops.size() && state.running(); i++) {
EntityOperation op = ops.get(i);
try { try {
Log.i(Helper.TAG, folder.name + if (message == null && !EntityOperation.SYNC.equals(op.name))
" start op=" + op.id + "/" + op.name + throw new MessageRemovedException();
" msg=" + op.message +
" args=" + op.args);
// Fetch most recent copy of message db.operation().setOperationError(op.id, null);
EntityMessage message = null; if (message != null)
if (op.message != null) db.message().setMessageError(message.id, null);
message = db.message().getMessage(op.message);
try { if (message != null && message.uid == null &&
if (message == null && !EntityOperation.SYNC.equals(op.name)) (EntityOperation.SEEN.equals(op.name) ||
throw new MessageRemovedException(); EntityOperation.DELETE.equals(op.name) ||
EntityOperation.MOVE.equals(op.name) ||
EntityOperation.HEADERS.equals(op.name)))
throw new IllegalArgumentException(op.name + " without uid " + op.args);
db.operation().setOperationError(op.id, null); JSONArray jargs = new JSONArray(op.args);
if (message != null)
db.message().setMessageError(message.id, null);
if (message != null && message.uid == null && // Operations should use database transaction when needed
(EntityOperation.SEEN.equals(op.name) ||
EntityOperation.DELETE.equals(op.name) ||
EntityOperation.MOVE.equals(op.name) ||
EntityOperation.HEADERS.equals(op.name)))
throw new IllegalArgumentException(op.name + " without uid " + op.args);
JSONArray jargs = new JSONArray(op.args); if (EntityOperation.SEEN.equals(op.name))
doSeen(folder, ifolder, message, jargs, db);
// Operations should use database transaction when needed else if (EntityOperation.ANSWERED.equals(op.name))
doAnswered(folder, ifolder, message, jargs, db);
if (EntityOperation.SEEN.equals(op.name)) else if (EntityOperation.FLAG.equals(op.name))
doSeen(folder, ifolder, message, jargs, db); doFlag(folder, ifolder, message, jargs, db);
else if (EntityOperation.ANSWERED.equals(op.name)) else if (EntityOperation.KEYWORD.equals(op.name))
doAnswered(folder, ifolder, message, jargs, db); doKeyword(folder, ifolder, message, jargs, db);
else if (EntityOperation.FLAG.equals(op.name)) else if (EntityOperation.ADD.equals(op.name))
doFlag(folder, ifolder, message, jargs, db); doAdd(folder, isession, istore, ifolder, message, jargs, db);
else if (EntityOperation.KEYWORD.equals(op.name)) else if (EntityOperation.MOVE.equals(op.name))
doKeyword(folder, ifolder, message, jargs, db); doMove(folder, isession, istore, ifolder, message, jargs, db);
else if (EntityOperation.ADD.equals(op.name)) else if (EntityOperation.DELETE.equals(op.name))
doAdd(folder, isession, istore, ifolder, message, jargs, db); doDelete(folder, ifolder, message, jargs, db);
else if (EntityOperation.MOVE.equals(op.name)) else if (EntityOperation.SEND.equals(op.name))
doMove(folder, isession, istore, ifolder, message, jargs, db); doSend(message, db);
else if (EntityOperation.DELETE.equals(op.name)) else if (EntityOperation.HEADERS.equals(op.name))
doDelete(folder, ifolder, message, jargs, db); doHeaders(folder, ifolder, message, db);
else if (EntityOperation.SEND.equals(op.name)) else if (EntityOperation.BODY.equals(op.name))
doSend(message, db); doBody(folder, ifolder, message, db);
else if (EntityOperation.HEADERS.equals(op.name)) else if (EntityOperation.ATTACHMENT.equals(op.name))
doHeaders(folder, ifolder, message, db); doAttachment(folder, op, ifolder, message, jargs, db);
else if (EntityOperation.BODY.equals(op.name))
doBody(folder, ifolder, message, db);
else if (EntityOperation.ATTACHMENT.equals(op.name))
doAttachment(folder, op, ifolder, message, jargs, db);
else if (EntityOperation.SYNC.equals(op.name))
if (!EntityFolder.OUTBOX.equals(folder.type))
synchronizeMessages(account, folder, ifolder, state);
else
db.folder().setFolderError(folder.id, null);
else if (EntityOperation.SYNC.equals(op.name))
if (!EntityFolder.OUTBOX.equals(folder.type))
synchronizeMessages(account, folder, ifolder, state);
else else
throw new MessagingException("Unknown operation name=" + op.name); db.folder().setFolderError(folder.id, null);
// Operation succeeded else
throw new MessagingException("Unknown operation name=" + op.name);
// Operation succeeded
db.operation().deleteOperation(op.id);
} catch (Throwable ex) {
// TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt
reportError(null, folder.name, ex);
db.operation().setOperationError(op.id, Helper.formatThrowable(ex));
if (message != null &&
!(ex instanceof MessageRemovedException) &&
!(ex instanceof FolderClosedException) &&
!(ex instanceof IllegalStateException))
db.message().setMessageError(message.id, Helper.formatThrowable(ex));
if (ex instanceof MessageRemovedException ||
ex instanceof FolderNotFoundException ||
ex instanceof SendFailedException) {
Log.w(Helper.TAG, "Unrecoverable " + ex + "\n" + Log.getStackTraceString(ex));
// There is no use in repeating
db.operation().deleteOperation(op.id); db.operation().deleteOperation(op.id);
} catch (Throwable ex) { continue;
// TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt } else if (ex instanceof MessagingException) {
reportError(null, folder.name, ex); // Socket timeout is a recoverable condition (send message)
if (ex.getCause() instanceof SocketTimeoutException) {
db.operation().setOperationError(op.id, Helper.formatThrowable(ex)); Log.w(Helper.TAG, "Recoverable " + ex + "\n" + Log.getStackTraceString(ex));
if (message != null && // No need to inform user
!(ex instanceof MessageRemovedException) && return;
!(ex instanceof FolderClosedException) &&
!(ex instanceof IllegalStateException))
db.message().setMessageError(message.id, Helper.formatThrowable(ex));
if (ex instanceof MessageRemovedException ||
ex instanceof FolderNotFoundException ||
ex instanceof SendFailedException) {
Log.w(Helper.TAG, "Unrecoverable " + ex + "\n" + Log.getStackTraceString(ex));
// There is no use in repeating
db.operation().deleteOperation(op.id);
continue;
} else if (ex instanceof MessagingException) {
// Socket timeout is a recoverable condition (send message)
if (ex.getCause() instanceof SocketTimeoutException) {
Log.w(Helper.TAG, "Recoverable " + ex + "\n" + Log.getStackTraceString(ex));
// No need to inform user
return;
}
} }
throw ex;
} }
} finally {
Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name); throw ex;
} }
} finally {
Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
} }
} finally {
Log.i(Helper.TAG, folder.name + " end process state=" + state);
} }
} finally {
Log.i(Helper.TAG, folder.name + " end process state=" + state);
} }
} }
@ -1536,13 +1528,23 @@ public class ServiceSynchronize extends LifecycleService {
Flags flags = new Flags(keyword); Flags flags = new Flags(keyword);
imessage.setFlags(flags, set); imessage.setFlags(flags, set);
List<String> keywords = new ArrayList<>(Arrays.asList(message.keywords)); try {
if (set) { db.beginTransaction();
if (!keywords.contains(keyword))
keywords.add(keyword); message = db.message().getMessage(message.id);
} else
keywords.remove(keyword); List<String> keywords = new ArrayList<>(Arrays.asList(message.keywords));
db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(keywords.toArray(new String[0]))); if (set) {
if (!keywords.contains(keyword))
keywords.add(keyword);
} else
keywords.remove(keyword);
db.message().setMessageKeywords(message.id, DB.Converters.fromStringArray(keywords.toArray(new String[0])));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} }
private void doAdd(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException { private void doAdd(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException {
@ -1672,6 +1674,9 @@ public class ServiceSynchronize extends LifecycleService {
try { try {
db.beginTransaction(); db.beginTransaction();
// Message could be moved
message = db.message().getMessage(message.id);
// Mark message as sent // Mark message as sent
// - will be moved to sent folder by synchronize message later // - will be moved to sent folder by synchronize message later
message.sent = imessage.getSentDate().getTime(); message.sent = imessage.getSentDate().getTime();