Observe operations

pull/146/head
M66B 6 years ago
parent 810fe66a78
commit 251ff98ea5

@ -105,6 +105,7 @@ The low priority status bar notification shows the number of pending operations,
* headers: download message headers
* body: download message text
* attachment: download attachment
* sync: synchronize local folder
Operations are processed only when there is a connection to the email server or when manually synchronizing.
See also [this FAQ](#user-content-faq16).

File diff suppressed because it is too large Load Diff

@ -564,8 +564,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
db.endTransaction();
}
EntityOperation.process(context);
return (draft == null ? null : draft.id);
}
@ -921,7 +919,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
for (EntityOperation op : db.operation().getOperations()) {
String line = String.format("%s %d %s %s %s\r\n",
DF.format(op.created),
op.message,
op.message == null ? -1 : op.message,
op.name,
op.args,
op.error);
@ -999,8 +997,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
db.endTransaction();
}
EntityOperation.process(context);
return draft.id;
}

@ -257,8 +257,6 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
db.endTransaction();
}
EntityOperation.process(context);
return null;
}

@ -36,6 +36,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.text.Collator;
import java.util.ArrayList;
@ -190,7 +191,6 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
PopupMenu popupMenu = new PopupMenu(context, itemView);
popupMenu.getMenu().add(Menu.NONE, action_synchronize_now, 1, R.string.title_synchronize_now);
popupMenu.getMenu().findItem(action_synchronize_now).setEnabled("connected".equals(folder.state));
if (!EntityFolder.DRAFTS.equals(folder.type))
popupMenu.getMenu().add(Menu.NONE, action_delete_local, 2, R.string.title_delete_local);
@ -232,12 +232,33 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
}
private void onActionSynchronizeNow() {
Log.i(Helper.TAG, folder.name + " requesting sync");
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ServiceSynchronize.ACTION_SYNCHRONIZE_FOLDER)
.setType("account/" + (folder.account == null ? "outbox" : Long.toString(folder.account)))
.putExtra("folder", folder.id));
Bundle args = new Bundle();
args.putLong("account", folder.account);
args.putLong("folder", folder.id);
new SimpleTask<EntityAccount>() {
@Override
protected EntityAccount onLoad(Context context, Bundle args) {
long account = args.getLong("account");
long folder = args.getLong("folder");
DB db = DB.getInstance(context);
EntityOperation.sync(db, folder);
return db.account().getAccount(account);
}
@Override
protected void onLoaded(Bundle args, EntityAccount account) {
if (!"connected".equals(account.state))
Toast.makeText(context, R.string.title_sync_queued, Toast.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.load(context, owner, args);
}
private void OnActionDeleteLocal() {
@ -295,8 +316,6 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
db.endTransaction();
}
EntityOperation.process(context);
return null;
}

@ -971,8 +971,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1120,8 +1118,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1153,7 +1149,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
EntityMessage message = db.message().getMessage(id);
db.message().setMessageUiFlagged(message.id, flagged);
EntityOperation.queue(db, message, EntityOperation.FLAG, flagged);
EntityOperation.process(context);
return null;
}
@ -1181,7 +1176,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
EntityOperation.queue(db, message, EntityOperation.HEADERS);
EntityOperation.process(context);
return null;
}
@ -1268,8 +1262,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1303,7 +1295,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
DB db = DB.getInstance(context);
EntityOperation.queue(db, message, EntityOperation.KEYWORD, keyword, true);
EntityOperation.process(context);
return null;
}
@ -1441,8 +1432,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
db.endTransaction();
}
EntityOperation.process(context);
return null;
}

@ -80,7 +80,7 @@ public class AdapterOperation extends RecyclerView.Adapter<AdapterOperation.View
}
private void bindTo(EntityOperation operation) {
tvMessage.setText(Long.toString(operation.message));
tvMessage.setText(operation.message == null ? null : Long.toString(operation.message));
tvName.setText(operation.name);
tvArgs.setText(operation.args);
tvTime.setText(df.format(new Date(operation.created)));
@ -95,6 +95,8 @@ public class AdapterOperation extends RecyclerView.Adapter<AdapterOperation.View
return;
EntityOperation operation = filtered.get(pos);
if (operation.message == null)
return;
Bundle args = new Bundle();
args.putLong("id", operation.message);
@ -134,7 +136,6 @@ public class AdapterOperation extends RecyclerView.Adapter<AdapterOperation.View
@Override
protected Void onLoad(Context context, Bundle args) throws Throwable {
DB.getInstance(context).operation().deleteOperation(args.getLong("id"));
EntityOperation.process(context);
return null;
}

@ -46,7 +46,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 11,
version = 12,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -199,6 +199,27 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `operation` ADD COLUMN `error` TEXT");
}
})
.addMigrations(new Migration(11, 12) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("DROP INDEX `index_operation_folder`");
db.execSQL("DROP INDEX `index_operation_message`");
db.execSQL("DROP TABLE `operation`");
db.execSQL("CREATE TABLE `operation`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
", `folder` INTEGER NOT NULL" +
", `message` INTEGER" +
", `name` TEXT NOT NULL" +
", `args` TEXT NOT NULL" +
", `created` INTEGER NOT NULL" +
", `error` TEXT" +
", FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE" +
", FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
db.execSQL("CREATE INDEX `index_operation_folder` ON `operation` (`folder`)");
db.execSQL("CREATE INDEX `index_operation_message` ON `operation` (`message`)");
}
})
.build();
}

@ -65,9 +65,9 @@ public interface DaoAccount {
@Query("SELECT" +
" (SELECT COUNT(account.id) FROM account WHERE synchronize AND state = 'connected') AS accounts" +
", (SELECT COUNT(operation.id) FROM operation" +
" JOIN message ON message.id = operation.message" +
" JOIN account ON account.id = message.account" +
" WHERE synchronize) AS operations" +
" JOIN folder ON folder.id = operation.folder" +
" JOIN account ON account.id = folder.account" +
" WHERE account.synchronize) AS operations" +
", (SELECT COUNT(message.id) FROM message" +
" JOIN folder ON folder.id = message.folder" +
" JOIN operation ON operation.message = message.id AND operation.name = '" + EntityOperation.SEND + "'" +

@ -37,11 +37,16 @@ public interface DaoOperation {
@Query("SELECT * FROM operation ORDER BY id")
LiveData<List<EntityOperation>> liveOperations();
@Query("SELECT * FROM operation WHERE folder = :folder ORDER BY id")
LiveData<List<EntityOperation>> liveOperations(long folder);
@Query("SELECT * FROM operation ORDER BY id")
List<EntityOperation> getOperations();
@Query("SELECT COUNT(id) FROM operation WHERE folder = :folder")
int getOperationCount(long folder);
@Query("SELECT COUNT(id) FROM operation" +
" WHERE folder = :folder" +
" AND (:name IS NULL OR operation.name = :name)")
int getOperationCount(long folder, String name);
@Query("UPDATE operation SET error = :error WHERE id = :id")
int setOperationError(long id, String error);

@ -19,18 +19,13 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
@ -56,7 +51,6 @@ public class EntityOperation {
public Long id;
@NonNull
public Long folder;
@NonNull
public Long message;
@NonNull
public String name;
@ -77,66 +71,51 @@ public class EntityOperation {
public static final String HEADERS = "headers";
public static final String BODY = "body";
public static final String ATTACHMENT = "attachment";
private static List<Intent> queue = new ArrayList<>();
public static final String SYNC = "sync";
static void queue(DB db, EntityMessage message, String name) {
JSONArray jargs = new JSONArray();
queue(db, message, name, jargs);
queue(db, message.folder, message.id, name, jargs);
}
static void queue(DB db, EntityMessage message, String name, Object value) {
JSONArray jargs = new JSONArray();
jargs.put(value);
queue(db, message, name, jargs);
queue(db, message.folder, message.id, name, jargs);
}
static void queue(DB db, EntityMessage message, String name, Object value1, Object value2) {
JSONArray jargs = new JSONArray();
jargs.put(value1);
jargs.put(value2);
queue(db, message, name, jargs);
queue(db, message.folder, message.id, name, jargs);
}
private static void queue(DB db, EntityMessage message, String name, JSONArray jargs) {
static void sync(DB db, long folder) {
if (db.operation().getOperationCount(folder, EntityOperation.SYNC) == 0)
queue(db, folder, null, EntityOperation.SYNC, new JSONArray());
}
private static void queue(DB db, long folder, Long message, String name, JSONArray jargs) {
EntityOperation operation = new EntityOperation();
operation.folder = message.folder;
operation.message = message.id;
operation.folder = folder;
operation.message = message;
operation.name = name;
operation.args = jargs.toString();
operation.created = new Date().getTime();
operation.id = db.operation().insertOperation(operation);
Intent intent = new Intent();
intent.setType("account/" + (SEND.equals(name) ? "outbox" : message.account));
intent.setAction(ServiceSynchronize.ACTION_PROCESS_OPERATIONS);
intent.putExtra("folder", message.folder);
synchronized (queue) {
queue.add(intent);
}
Log.i(Helper.TAG, "Queued op=" + operation.id + "/" + operation.name +
" msg=" + message.folder + "/" + operation.message +
" msg=" + operation.folder + "/" + operation.message +
" args=" + operation.args);
}
public static void process(Context context) {
// Processing needs to be done after committing to the database
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
synchronized (queue) {
for (Intent intent : queue)
lbm.sendBroadcast(intent);
queue.clear();
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityOperation) {
EntityOperation other = (EntityOperation) obj;
return (this.folder.equals(other.folder) &&
this.message.equals(other.message) &&
(this.message == null ? other.message == null : this.message.equals(other.message)) &&
this.name.equals(other.name) &&
this.args.equals(other.args) &&
this.created.equals(other.created) &&

@ -1267,8 +1267,6 @@ public class FragmentCompose extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return result;
}
@ -1651,8 +1649,6 @@ public class FragmentCompose extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return draft;
}

@ -21,7 +21,6 @@ package eu.faircode.email;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@ -33,6 +32,7 @@ import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar;
import com.sun.mail.imap.IMAPFolder;
@ -46,7 +46,6 @@ import javax.mail.Session;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class FragmentFolder extends FragmentEx {
private ViewGroup view;
@ -213,11 +212,33 @@ public class FragmentFolder extends FragmentEx {
if (folder == null || !folder.name.equals(name))
ServiceSynchronize.reload(getContext(), "save folder");
else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ServiceSynchronize.ACTION_SYNCHRONIZE_FOLDER)
.setType("account/" + folder.account)
.putExtra("folder", folder.id));
Bundle sargs = new Bundle();
sargs.putLong("account", folder.account);
sargs.putLong("folder", folder.id);
new SimpleTask<EntityAccount>() {
@Override
protected EntityAccount onLoad(Context context, Bundle args) {
long account = args.getLong("account");
long folder = args.getLong("folder");
DB db = DB.getInstance(context);
EntityOperation.sync(db, folder);
return db.account().getAccount(account);
}
@Override
protected void onLoaded(Bundle args, EntityAccount account) {
if (!"connected".equals(account.state))
Toast.makeText(getContext(), R.string.title_sync_queued, Toast.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.load(FragmentFolder.this, sargs);
}
return null;

@ -701,8 +701,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -743,8 +741,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -826,8 +822,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return result;
}
@ -891,8 +885,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1261,8 +1253,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1509,8 +1499,6 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@ -1657,9 +1645,6 @@ public class FragmentMessages extends FragmentEx {
} finally {
db.endTransaction();
}
EntityOperation.process(snackbar.getContext());
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}

@ -181,8 +181,6 @@ public class FragmentOptions extends FragmentEx implements SharedPreferences.OnS
}
}
EntityOperation.process(context);
return null;
}

@ -224,8 +224,6 @@ public class Helper {
db.endTransaction();
}
EntityOperation.process(context);
return draft.id;
}

@ -40,6 +40,8 @@ import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
@ -120,12 +122,10 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
public class ServiceSynchronize extends LifecycleService {
private final Object lock = new Object();
private TupleAccountStats lastStats = null;
private ServiceManager serviceManager = new ServiceManager();
private static ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@ -149,9 +149,6 @@ public class ServiceSynchronize extends LifecycleService {
static final int PI_TRASH = 5;
static final int PI_IGNORED = 6;
static final String ACTION_SYNCHRONIZE_FOLDER = BuildConfig.APPLICATION_ID + ".SYNCHRONIZE_FOLDER";
static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS";
@Override
public void onCreate() {
Log.i(Helper.TAG, "Service create version=" + BuildConfig.VERSION_NAME);
@ -356,8 +353,6 @@ public class ServiceSynchronize extends LifecycleService {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
}.load(this, args);
@ -734,14 +729,12 @@ public class ServiceSynchronize extends LifecycleService {
private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException {
final PowerManager pm = getSystemService(PowerManager.class);
final PowerManager.WakeLock wl0 = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":account." + account.id + ".monitor");
final PowerManager.WakeLock wlAccount = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":account." + account.id);
try {
wl0.acquire();
wlAccount.acquire();
final DB db = DB.getInstance(this);
final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
int backoff = CONNECT_BACKOFF_START;
while (state.running()) {
@ -760,19 +753,15 @@ public class ServiceSynchronize extends LifecycleService {
final IMAPStore istore = (IMAPStore) isession.getStore(account.starttls ? "imap" : "imaps");
final Map<EntityFolder, IMAPFolder> folders = new HashMap<>();
List<Thread> syncs = new ArrayList<>();
List<Thread> idlers = new ArrayList<>();
List<Handler> handlers = new ArrayList<>();
try {
// Listen for store events
istore.addStoreListener(new StoreListener() {
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":account." + account.id + ".store");
@Override
public void notification(StoreEvent e) {
try {
wl.acquire();
wlAccount.acquire();
String type = (e.getMessageType() == StoreEvent.ALERT ? "alert" : "notice");
EntityLog.log(ServiceSynchronize.this, account.name + " " + type + ": " + e.getMessage());
if (e.getMessageType() == StoreEvent.ALERT) {
@ -780,32 +769,28 @@ public class ServiceSynchronize extends LifecycleService {
state.error();
}
} finally {
wl.release();
wlAccount.release();
}
}
});
// Listen for folder events
istore.addFolderListener(new FolderAdapter() {
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":account." + account.id + ".folder");
@Override
public void folderCreated(FolderEvent e) {
try {
wl.acquire();
wlAccount.acquire();
Log.i(Helper.TAG, "Folder created=" + e.getFolder().getFullName());
reload(ServiceSynchronize.this, "folder created");
} finally {
wl.release();
wlAccount.release();
}
}
@Override
public void folderRenamed(FolderEvent e) {
try {
wl.acquire();
wlAccount.acquire();
Log.i(Helper.TAG, "Folder renamed=" + e.getFolder());
String old = e.getFolder().getFullName();
@ -815,18 +800,18 @@ public class ServiceSynchronize extends LifecycleService {
reload(ServiceSynchronize.this, "folder renamed");
} finally {
wl.release();
wlAccount.release();
}
}
@Override
public void folderDeleted(FolderEvent e) {
try {
wl.acquire();
wlAccount.acquire();
Log.i(Helper.TAG, "Folder deleted=" + e.getFolder().getFullName());
reload(ServiceSynchronize.this, "folder deleted");
} finally {
wl.release();
wlAccount.release();
}
}
});
@ -909,27 +894,13 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, account.name + " folder " + folder.name + " flags=" + ifolder.getPermanentFlags());
// Synchronize folder
Thread sync = new Thread(new Runnable() {
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":account." + account.id + ".sync");
@Override
public void run() {
try {
wl.acquire();
// Process pending operations
processOperations(folder, isession, istore, ifolder, state);
// Listen for new and deleted messages
ifolder.addMessageCountListener(new MessageCountAdapter() {
@Override
public void messagesAdded(MessageCountEvent e) {
synchronized (lock) {
synchronized (folder) {
try {
wl.acquire();
wlAccount.acquire();
Log.i(Helper.TAG, folder.name + " messages added");
FetchProfile fp = new FetchProfile();
@ -956,7 +927,14 @@ public class ServiceSynchronize extends LifecycleService {
} 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) {
@ -965,23 +943,22 @@ public class ServiceSynchronize extends LifecycleService {
else
throw ex;
}
EntityOperation.process(ServiceSynchronize.this); // download small attachments
} 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 {
wl.release();
wlAccount.release();
}
}
}
@Override
public void messagesRemoved(MessageCountEvent e) {
synchronized (lock) {
synchronized (folder) {
try {
wl.acquire();
wlAccount.acquire();
Log.i(Helper.TAG, folder.name + " messages removed");
for (Message imessage : e.getMessages())
try {
@ -1000,24 +977,21 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wl.release();
wlAccount.release();
}
}
}
});
// Fetch e-mail
synchronizeMessages(account, folder, ifolder, state);
// 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 (lock) {
synchronized (folder) {
try {
wl.acquire();
wlAccount.acquire();
try {
Log.i(Helper.TAG, folder.name + " message changed");
@ -1037,7 +1011,14 @@ public class ServiceSynchronize extends LifecycleService {
} 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) {
@ -1052,23 +1033,11 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
state.error();
} finally {
wl.release();
wlAccount.release();
}
}
}
});
} 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 {
wl.release();
}
}
}, "sync." + folder.id);
sync.start();
syncs.add(sync);
// Idle folder
if (capIdle) {
@ -1078,9 +1047,8 @@ public class ServiceSynchronize extends LifecycleService {
try {
Log.i(Helper.TAG, folder.name + " start idle");
while (state.running()) {
Log.i(Helper.TAG, folder.name + " do idle");
Log.v(Helper.TAG, folder.name + " do idle");
ifolder.idle(false);
//Log.i(Helper.TAG, folder.name + " done idle");
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
@ -1095,107 +1063,109 @@ public class ServiceSynchronize extends LifecycleService {
idler.start();
idlers.add(idler);
}
EntityOperation.sync(db, folder.id);
}
// Process folder actions
BroadcastReceiver processFolder = new BroadcastReceiver() {
// Observe folder operations
for (final EntityFolder folder : db.folder().getFolders(account.id)) {
Handler handler = new Handler(getMainLooper()) {
private List<Long> handling = new ArrayList<>();
private final PowerManager.WakeLock wlFolder = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":folder." + folder.id);
private final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@Override
public void onReceive(Context context, final Intent intent) {
public void handleMessage(android.os.Message msg) {
Log.i(Helper.TAG, folder.name + " observe=" + msg.what);
if (msg.what == 0)
db.operation().liveOperations(folder.id).removeObservers(ServiceSynchronize.this);
else
db.operation().liveOperations(folder.id).observe(ServiceSynchronize.this, new Observer<List<EntityOperation>>() {
@Override
public void onChanged(List<EntityOperation> operations) {
boolean process = false;
List<Long> current = new ArrayList<>();
for (EntityOperation op : operations) {
if (!handling.contains(op.id) || op.error != null)
process = true;
current.add(op.id);
}
handling = current;
if (handling.size() > 0 && process) {
Log.i(Helper.TAG, folder.name + " operations=" + operations.size());
executor.submit(new Runnable() {
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":account." + account.id + ".process");
@Override
public void run() {
long fid = intent.getLongExtra("folder", -1);
try {
wl.acquire();
Log.i(Helper.TAG, "Process folder=" + fid + " intent=" + intent);
wlFolder.acquire();
Log.i(Helper.TAG, folder.name + " process");
// Get folder
EntityFolder folder = null;
EntityFolder ofolder = null;
IMAPFolder ifolder = null;
for (EntityFolder f : folders.keySet())
if (f.id == fid) {
folder = f;
if (f.id == folder.id) {
ofolder = f;
ifolder = folders.get(f);
break;
}
final boolean shouldClose = (folder == null);
final boolean shouldClose = (ofolder == null);
try {
if (folder == null)
folder = db.folder().getFolder(fid);
if (ofolder == null)
ofolder = db.folder().getFolder(folder.id);
Log.i(Helper.TAG, folder.name + " run " + (shouldClose ? "offline" : "online"));
Log.i(Helper.TAG, ofolder.name + " run " + (shouldClose ? "offline" : "online"));
if (ifolder == null) {
// Prevent unnecessary folder connections
if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction()))
if (db.operation().getOperationCount(fid) == 0)
if (db.operation().getOperationCount(ofolder.id, null) == 0)
return;
db.folder().setFolderState(folder.id, "connecting");
db.folder().setFolderState(ofolder.id, "connecting");
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder = (IMAPFolder) istore.getFolder(ofolder.name);
ifolder.open(Folder.READ_WRITE);
db.folder().setFolderState(folder.id, "connected");
db.folder().setFolderError(folder.id, null);
db.folder().setFolderState(ofolder.id, "connected");
db.folder().setFolderError(ofolder.id, null);
}
if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction()))
processOperations(folder, isession, istore, ifolder, state);
else if (ACTION_SYNCHRONIZE_FOLDER.equals(intent.getAction())) {
processOperations(folder, isession, istore, ifolder, state);
synchronizeMessages(account, folder, ifolder, state);
}
processOperations(account, ofolder, isession, istore, ifolder, state);
} 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));
Log.e(Helper.TAG, ofolder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, ofolder.name, ex);
db.folder().setFolderError(ofolder.id, Helper.formatThrowable(ex));
state.error();
} finally {
if (shouldClose) {
if (ifolder != null && ifolder.isOpen()) {
db.folder().setFolderState(folder.id, "closing");
db.folder().setFolderState(ofolder.id, "closing");
try {
ifolder.close(false);
} catch (MessagingException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
Log.w(Helper.TAG, ofolder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
}
}
db.folder().setFolderState(folder.id, null);
db.folder().setFolderState(ofolder.id, null);
}
}
} finally {
wl.release();
wlFolder.release();
}
}
});
}
}
});
}
};
// Listen for folder operations
IntentFilter f = new IntentFilter();
f.addAction(ACTION_SYNCHRONIZE_FOLDER);
f.addAction(ACTION_PROCESS_OPERATIONS);
f.addDataType("account/" + account.id);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
lbm.registerReceiver(processFolder, f);
for (EntityFolder folder : folders.keySet())
if (db.operation().getOperationCount(folder.id) > 0) {
Intent intent = new Intent();
intent.setType("account/" + account.id);
intent.setAction(ServiceSynchronize.ACTION_PROCESS_OPERATIONS);
intent.putExtra("folder", folder.id);
lbm.sendBroadcast(intent);
handler.sendEmptyMessage(1);
handlers.add(handler);
}
// Keep alive alarm receiver
@ -1204,7 +1174,7 @@ public class ServiceSynchronize extends LifecycleService {
public void onReceive(Context context, Intent intent) {
// Receiver runs on main thread
// Receiver has a wake lock for ~10 seconds
EntityLog.log(context, account.name + " keep alive wake lock=" + wl0.isHeld());
EntityLog.log(context, account.name + " keep alive wake lock=" + wlAccount.isHeld());
state.release();
}
};
@ -1242,19 +1212,22 @@ public class ServiceSynchronize extends LifecycleService {
pi);
try {
wl0.release();
wlAccount.release();
state.acquire();
} catch (InterruptedException ex) {
EntityLog.log(this, account.name + " waited state=" + state);
} finally {
wl0.acquire();
wlAccount.acquire();
}
}
} finally {
// Cleanup
am.cancel(pi);
unregisterReceiver(alarm);
lbm.unregisterReceiver(processFolder);
for (Handler handler : handlers)
handler.sendEmptyMessage(0);
handlers.clear();
}
Log.i(Helper.TAG, account.name + " done state=" + state);
@ -1282,13 +1255,10 @@ public class ServiceSynchronize extends LifecycleService {
db.account().setAccountState(account.id, null);
}
// Stop syncs
for (Thread sync : syncs)
state.join(sync);
// Stop idlers
for (Thread idler : idlers)
state.join(idler);
idlers.clear();
for (EntityFolder folder : folders.keySet())
db.folder().setFolderState(folder.id, null);
@ -1323,10 +1293,10 @@ public class ServiceSynchronize extends LifecycleService {
pi);
try {
wl0.release();
wlAccount.release();
state.acquire(2 * CONNECT_BACKOFF_AlARM * 60 * 1000L);
} finally {
wl0.acquire();
wlAccount.acquire();
}
} finally {
// Cleanup
@ -1343,12 +1313,12 @@ public class ServiceSynchronize extends LifecycleService {
}
} finally {
EntityLog.log(this, account.name + " stopped");
wl0.release();
wlAccount.release();
}
}
private void processOperations(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, ServiceState state) throws MessagingException, JSONException, IOException {
synchronized (lock) {
private void processOperations(EntityAccount account, EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, ServiceState state) throws MessagingException, JSONException, IOException {
synchronized (folder) {
try {
Log.i(Helper.TAG, folder.name + " start process");
@ -1363,15 +1333,20 @@ public class ServiceSynchronize extends LifecycleService {
" msg=" + op.message +
" args=" + op.args);
EntityMessage message = db.message().getMessage(op.message);
// Fetch most recent copy of message
EntityMessage message = null;
if (op.message != null)
message = db.message().getMessage(op.message);
try {
if (message == null)
if (message == null && !EntityOperation.SYNC.equals(op.name))
throw new MessageRemovedException();
db.operation().setOperationError(op.id, null);
if (message != null)
db.message().setMessageError(message.id, null);
if (message.uid == null &&
if (message != null && message.uid == null &&
(EntityOperation.SEEN.equals(op.name) ||
EntityOperation.DELETE.equals(op.name) ||
EntityOperation.MOVE.equals(op.name) ||
@ -1380,6 +1355,8 @@ public class ServiceSynchronize extends LifecycleService {
JSONArray jargs = new JSONArray(op.args);
// Operations should use database transaction when needed
if (EntityOperation.SEEN.equals(op.name))
doSeen(folder, ifolder, message, jargs, db);
@ -1413,6 +1390,9 @@ public class ServiceSynchronize extends LifecycleService {
else if (EntityOperation.ATTACHMENT.equals(op.name))
doAttachment(folder, op, ifolder, message, jargs, db);
else if (EntityOperation.SYNC.equals(op.name))
synchronizeMessages(account, folder, ifolder, state);
else
throw new MessagingException("Unknown operation name=" + op.name);
@ -1701,8 +1681,6 @@ public class ServiceSynchronize extends LifecycleService {
} finally {
db.endTransaction();
}
EntityOperation.process(this);
} catch (MessagingException ex) {
db.identity().setIdentityError(ident.id, Helper.formatThrowable(ex));
@ -1890,20 +1868,16 @@ public class ServiceSynchronize extends LifecycleService {
long fetch = SystemClock.elapsedRealtime();
Log.i(Helper.TAG, folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
for (int i = 0; i < imessages.length && state.running(); i++) {
Message imessage = imessages[i];
for (int i = 0; i < imessages.length && state.running(); i++)
try {
uids.remove(ifolder.getUID(imessage));
uids.remove(ifolder.getUID(imessages[i]));
} 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));
}
}
// Delete local messages not at remote
Log.i(Helper.TAG, folder.name + " delete=" + uids.size());
@ -1926,8 +1900,6 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " add=" + imessages.length);
for (int i = imessages.length - 1; i >= 0 && state.running(); i -= SYNC_BATCH_SIZE) {
int from = Math.max(0, i - SYNC_BATCH_SIZE + 1);
//Log.i(Helper.TAG, folder.name + " update " + from + " .. " + i);
Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
// Full fetch new/changed messages only
@ -1983,18 +1955,18 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " download=" + imessages.length);
for (int i = imessages.length - 1; i >= 0 && state.running(); i -= DOWNLOAD_BATCH_SIZE) {
int from = Math.max(0, i - DOWNLOAD_BATCH_SIZE + 1);
//Log.i(Helper.TAG, folder.name + " download " + from + " .. " + i);
Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
// Fetch on demand
for (int j = isub.length - 1; j >= 0 && state.running(); j--)
try {
//Log.i(Helper.TAG, folder.name + " download index=" + (from + j) + " id=" + ids[from + j]);
db.beginTransaction();
if (ids[from + j] != null) {
downloadMessage(this, folder, ifolder, (IMAPMessage) isub[j], ids[from + j]);
Thread.sleep(20);
}
db.setTransactionSuccessful();
} catch (FolderClosedException ex) {
throw ex;
} catch (FolderClosedIOException ex) {
@ -2002,6 +1974,7 @@ public class ServiceSynchronize extends LifecycleService {
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
db.endTransaction();
// Free memory
((IMAPMessage) isub[j]).invalidateHeaders();
}
@ -2051,7 +2024,6 @@ public class ServiceSynchronize extends LifecycleService {
if (message == null) {
// Will fetch headers within database transaction
String msgid = helper.getMessageID();
String[] refs = helper.getReferences();
Log.i(Helper.TAG, "Searching for " + msgid);
for (EntityMessage dup : db.message().getMessageByMsgId(folder.account, msgid, found)) {
EntityFolder dfolder = db.folder().getFolder(dup.folder);
@ -2302,9 +2274,7 @@ public class ServiceSynchronize extends LifecycleService {
private boolean started = false;
private int queued = 0;
private long lastLost = 0;
private EntityFolder outbox = null;
private ExecutorService queue = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
@ -2380,8 +2350,7 @@ public class ServiceSynchronize extends LifecycleService {
state.runnable(new Runnable() {
PowerManager pm = getSystemService(PowerManager.class);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":start");
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":main");
private List<ServiceState> threadState = new ArrayList<>();
@Override
@ -2391,12 +2360,6 @@ public class ServiceSynchronize extends LifecycleService {
final DB db = DB.getInstance(ServiceSynchronize.this);
outbox = db.folder().getOutbox();
if (outbox == null) {
EntityLog.log(ServiceSynchronize.this, "No outbox");
return;
}
long ago = new Date().getTime() - lastLost;
if (ago < RECONNECT_BACKOFF)
try {
@ -2409,19 +2372,69 @@ public class ServiceSynchronize extends LifecycleService {
}
// Start monitoring outbox
IntentFilter f = new IntentFilter();
f.addAction(ACTION_SYNCHRONIZE_FOLDER);
f.addAction(ACTION_PROCESS_OPERATIONS);
f.addDataType("account/outbox");
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
lbm.registerReceiver(outboxReceiver, f);
db.folder().setFolderState(outbox.id, "connected");
Handler handler = null;
final EntityFolder outbox = db.folder().getOutbox();
if (outbox != null) {
db.folder().setFolderError(outbox.id, null);
lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS)
.setType("account/outbox")
.putExtra("folder", outbox.id));
handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(android.os.Message msg) {
Log.i(Helper.TAG, outbox.name + " observe=" + msg.what);
if (msg.what == 0)
db.operation().liveOperations(outbox.id).removeObservers(ServiceSynchronize.this);
else {
db.operation().liveOperations(outbox.id).observe(ServiceSynchronize.this, new Observer<List<EntityOperation>>() {
private List<Long> handling = new ArrayList<>();
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@Override
public void onChanged(List<EntityOperation> operations) {
boolean process = false;
List<Long> current = new ArrayList<>();
for (EntityOperation op : operations) {
if (!handling.contains(op.id) || op.error != null)
process = true;
current.add(op.id);
}
handling = current;
if (handling.size() > 0 && process) {
Log.i(Helper.TAG, outbox.name + " operations=" + operations.size());
executor.submit(new Runnable() {
PowerManager pm = getSystemService(PowerManager.class);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":outbox");
@Override
public void run() {
try {
wl.acquire();
Log.i(Helper.TAG, outbox.name + " process");
db.folder().setFolderState(outbox.id, "syncing");
processOperations(null, outbox, null, null, null, state);
db.folder().setFolderError(outbox.id, null);
} catch (Throwable ex) {
Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(null, outbox.name, ex);
db.folder().setFolderError(outbox.id, Helper.formatThrowable(ex));
} finally {
db.folder().setFolderState(outbox.id, null);
wl.release();
EntityLog.log(ServiceSynchronize.this, "Outbox wake lock=" + wl.isHeld());
}
}
});
}
}
});
}
}
};
handler.sendEmptyMessage(1);
}
// Start monitoring accounts
List<EntityAccount> accounts = db.account().getAccounts(true);
@ -2463,9 +2476,11 @@ public class ServiceSynchronize extends LifecycleService {
threadState.clear();
// Stop monitoring outbox
lbm.unregisterReceiver(outboxReceiver);
if (outbox != null) {
Log.i(Helper.TAG, outbox.name + " unlisten operations");
handler.sendEmptyMessage(0);
db.folder().setFolderState(outbox.id, null);
}
EntityLog.log(ServiceSynchronize.this, "Main exited");
} catch (Throwable ex) {
@ -2505,8 +2520,7 @@ public class ServiceSynchronize extends LifecycleService {
queue.submit(new Runnable() {
PowerManager pm = getSystemService(PowerManager.class);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":reload");
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":manage");
@Override
public void run() {
@ -2546,45 +2560,6 @@ public class ServiceSynchronize extends LifecycleService {
started = doStart;
}
private BroadcastReceiver outboxReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, Intent intent) {
Log.v(Helper.TAG, outbox.name + " run operations");
executor.submit(new Runnable() {
PowerManager pm = getSystemService(PowerManager.class);
PowerManager.WakeLock wl = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":outbox");
@Override
public void run() {
try {
wl.acquire();
DB db = DB.getInstance(context);
try {
Log.i(Helper.TAG, outbox.name + " start operations");
db.folder().setFolderState(outbox.id, "syncing");
processOperations(outbox, null, null, null, state);
db.folder().setFolderError(outbox.id, null);
} catch (Throwable ex) {
Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(null, outbox.name, ex);
db.folder().setFolderError(outbox.id, Helper.formatThrowable(ex));
} finally {
Log.i(Helper.TAG, outbox.name + " end operations");
db.folder().setFolderState(outbox.id, null);
}
} finally {
wl.release();
EntityLog.log(ServiceSynchronize.this, "Outbox wake lock=" + wl.isHeld());
}
}
});
}
};
}
public static void init(Context context) {

@ -240,6 +240,7 @@
<string name="title_ask_spam">Report message as spam?</string>
<string name="title_ask_show_html">Showing the original message can leak privacy sensitive information</string>
<string name="title_ask_show_image">Showing images can leak privacy sensitive information</string>
<string name="title_sync_queued">Synchronization will take place on next account connection</string>
<string name="title_fix">Fix</string>
<string name="title_compose">Compose</string>

Loading…
Cancel
Save