diff --git a/FAQ.md b/FAQ.md index ad86918445..552f30ef27 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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). diff --git a/app/schemas/eu.faircode.email.DB/12.json b/app/schemas/eu.faircode.email.DB/12.json new file mode 100644 index 0000000000..a83720d7f9 --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/12.json @@ -0,0 +1,1154 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "84b376180b0565f3ddf0918dfb84904d", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `color` INTEGER, `signature` TEXT, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent_folder", + "columnName": "sent_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_identity_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_identity_account_email", + "unique": false, + "columnNames": [ + "account", + "email" + ], + "createSql": "CREATE INDEX `index_identity_account_email` ON `${TABLE_NAME}` (`account`, `email`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `signature` TEXT, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `color` INTEGER, `notify` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `created` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `display` TEXT, `hide` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `keywords` TEXT, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sync_days", + "columnName": "sync_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_days", + "columnName": "keep_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unified", + "columnName": "unified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_folder_account_name", + "unique": true, + "columnNames": [ + "account", + "name" + ], + "createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)" + }, + { + "name": "index_folder_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_folder_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_folder_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_folder_unified", + "unique": false, + "columnNames": [ + "unified" + ], + "createSql": "CREATE INDEX `index_folder_unified` ON `${TABLE_NAME}` (`unified`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `folder` INTEGER NOT NULL, `identity` INTEGER, `extra` TEXT, `replying` INTEGER, `forwarding` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `deliveredto` TEXT, `inreplyto` TEXT, `thread` TEXT, `avatar` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `subject` TEXT, `size` INTEGER, `content` INTEGER NOT NULL, `preview` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `answered` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `keywords` TEXT, `ui_seen` INTEGER NOT NULL, `ui_answered` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` INTEGER NOT NULL, `ui_browsed` INTEGER NOT NULL, `error` TEXT, `last_attempt` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`forwarding`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extra", + "columnName": "extra", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forwarding", + "columnName": "forwarding", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "msgid", + "columnName": "msgid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "references", + "columnName": "references", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deliveredto", + "columnName": "deliveredto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "headers", + "columnName": "headers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preview", + "columnName": "preview", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sent", + "columnName": "sent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stored", + "columnName": "stored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "answered", + "columnName": "answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_answered", + "columnName": "ui_answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_flagged", + "columnName": "ui_flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_hide", + "columnName": "ui_hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_found", + "columnName": "ui_found", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_ignored", + "columnName": "ui_ignored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_browsed", + "columnName": "ui_browsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_attempt", + "columnName": "last_attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_message_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_message_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_message_identity", + "unique": false, + "columnNames": [ + "identity" + ], + "createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)" + }, + { + "name": "index_message_replying", + "unique": false, + "columnNames": [ + "replying" + ], + "createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)" + }, + { + "name": "index_message_forwarding", + "unique": false, + "columnNames": [ + "forwarding" + ], + "createSql": "CREATE INDEX `index_message_forwarding` ON `${TABLE_NAME}` (`forwarding`)" + }, + { + "name": "index_message_folder_uid_ui_found", + "unique": true, + "columnNames": [ + "folder", + "uid", + "ui_found" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_folder_uid_ui_found` ON `${TABLE_NAME}` (`folder`, `uid`, `ui_found`)" + }, + { + "name": "index_message_msgid_folder_ui_found", + "unique": true, + "columnNames": [ + "msgid", + "folder", + "ui_found" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder_ui_found` ON `${TABLE_NAME}` (`msgid`, `folder`, `ui_found`)" + }, + { + "name": "index_message_thread", + "unique": false, + "columnNames": [ + "thread" + ], + "createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)" + }, + { + "name": "index_message_received", + "unique": false, + "columnNames": [ + "received" + ], + "createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)" + }, + { + "name": "index_message_ui_seen", + "unique": false, + "columnNames": [ + "ui_seen" + ], + "createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)" + }, + { + "name": "index_message_ui_hide", + "unique": false, + "columnNames": [ + "ui_hide" + ], + "createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + }, + { + "name": "index_message_ui_ignored", + "unique": false, + "columnNames": [ + "ui_ignored" + ], + "createSql": "CREATE INDEX `index_message_ui_ignored` ON `${TABLE_NAME}` (`ui_ignored`)" + }, + { + "name": "index_message_ui_browsed", + "unique": false, + "columnNames": [ + "ui_browsed" + ], + "createSql": "CREATE INDEX `index_message_ui_browsed` ON `${TABLE_NAME}` (`ui_browsed`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "identity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "forwarding" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `cid` TEXT, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sequence", + "columnName": "sequence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_attachment_message_sequence", + "unique": true, + "columnNames": [ + "message", + "sequence" + ], + "createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)" + }, + { + "name": "index_attachment_message_cid", + "unique": true, + "columnNames": [ + "message", + "cid" + ], + "createSql": "CREATE UNIQUE INDEX `index_attachment_message_cid` ON `${TABLE_NAME}` (`message`, `cid`)" + } + ], + "foreignKeys": [ + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "operation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_operation_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_operation_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_log_time", + "unique": false, + "columnNames": [ + "time" + ], + "createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"84b376180b0565f3ddf0918dfb84904d\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 43193ab435..5668bc0e23 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -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; } diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index 0180ce1757..7787544150 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -257,8 +257,6 @@ public class AdapterAttachment extends RecyclerView.Adapter() { + @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> liveOperations(); + @Query("SELECT * FROM operation WHERE folder = :folder ORDER BY id") + LiveData> liveOperations(long folder); + @Query("SELECT * FROM operation ORDER BY id") List 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); diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index 80523c9285..b0e0978876 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -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 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) && diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index e08ec51ce9..59c75a4673 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -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; } diff --git a/app/src/main/java/eu/faircode/email/FragmentFolder.java b/app/src/main/java/eu/faircode/email/FragmentFolder.java index 7448e65c7f..3f5f8f29d7 100644 --- a/app/src/main/java/eu/faircode/email/FragmentFolder.java +++ b/app/src/main/java/eu/faircode/email/FragmentFolder.java @@ -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() { + @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; diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 1e867b4c8f..983e027263 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -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)); } diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java index 7407768c29..ac29baee66 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptions.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java @@ -181,8 +181,6 @@ public class FragmentOptions extends FragmentEx implements SharedPreferences.OnS } } - EntityOperation.process(context); - return null; } diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index 8c436c839f..41cc72130c 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -224,8 +224,6 @@ public class Helper { db.endTransaction(); } - EntityOperation.process(context); - return draft.id; } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 0a1afd9619..7da8b6199b 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -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 folders = new HashMap<>(); - List syncs = new ArrayList<>(); List idlers = new ArrayList<>(); + List 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,166 +894,150 @@ 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"); - + // Listen for new and deleted messages + ifolder.addMessageCountListener(new MessageCountAdapter() { @Override - public void run() { - try { - wl.acquire(); + public void messagesAdded(MessageCountEvent e) { + synchronized (folder) { + try { + wlAccount.acquire(); + Log.i(Helper.TAG, folder.name + " messages added"); - // Process pending operations - processOperations(folder, isession, istore, ifolder, state); + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.FLAGS); + fp.add(FetchProfile.Item.CONTENT_INFO); // body structure + fp.add(UIDFolder.FetchProfileItem.UID); + fp.add(IMAPFolder.FetchProfileItem.HEADERS); + fp.add(IMAPFolder.FetchProfileItem.MESSAGE); + fp.add(FetchProfile.Item.SIZE); + fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); + ifolder.fetch(e.getMessages(), fp); - // Listen for new and deleted messages - ifolder.addMessageCountListener(new MessageCountAdapter() { - @Override - public void messagesAdded(MessageCountEvent e) { - synchronized (lock) { + for (Message imessage : e.getMessages()) + try { + long id; try { - wl.acquire(); - Log.i(Helper.TAG, folder.name + " messages added"); - - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.ENVELOPE); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.CONTENT_INFO); // body structure - fp.add(UIDFolder.FetchProfileItem.UID); - fp.add(IMAPFolder.FetchProfileItem.HEADERS); - fp.add(IMAPFolder.FetchProfileItem.MESSAGE); - fp.add(FetchProfile.Item.SIZE); - fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); - ifolder.fetch(e.getMessages(), fp); - - 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(); - } - downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id); - } 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; - } - 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(); + db.beginTransaction(); + id = synchronizeMessage( + ServiceSynchronize.this, + folder, ifolder, (IMAPMessage) imessage, + false, false, false); + db.setTransactionSuccessful(); } finally { - wl.release(); + db.endTransaction(); } - } - } - @Override - public void messagesRemoved(MessageCountEvent e) { - synchronized (lock) { try { - wl.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(); + db.beginTransaction(); + downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id); + db.setTransactionSuccessful(); } finally { - wl.release(); + 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; } - } - }); - - // 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) { - try { - wl.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(); - } - downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), id); - } 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 { - wl.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(); + } 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(); + } } } - }, "sync." + folder.id); - sync.start(); - syncs.add(sync); + + @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 { + 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(); + } + } + } + }); // 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,108 +1063,110 @@ public class ServiceSynchronize extends LifecycleService { idler.start(); idlers.add(idler); } + + EntityOperation.sync(db, folder.id); } - // Process folder actions - BroadcastReceiver processFolder = new BroadcastReceiver() { - @Override - public void onReceive(Context context, final Intent intent) { - executor.submit(new Runnable() { - PowerManager.WakeLock wl = pm.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, - BuildConfig.APPLICATION_ID + ":account." + account.id + ".process"); + // Observe folder operations + for (final EntityFolder folder : db.folder().getFolders(account.id)) { + Handler handler = new Handler(getMainLooper()) { + private List 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 run() { - long fid = intent.getLongExtra("folder", -1); - try { - wl.acquire(); - Log.i(Helper.TAG, "Process folder=" + fid + " intent=" + intent); - - // Get folder - EntityFolder folder = null; - IMAPFolder ifolder = null; - for (EntityFolder f : folders.keySet()) - if (f.id == fid) { - folder = f; - ifolder = folders.get(f); - break; + @Override + 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>() { + @Override + public void onChanged(List operations) { + boolean process = false; + List current = new ArrayList<>(); + for (EntityOperation op : operations) { + if (!handling.contains(op.id) || op.error != null) + process = true; + current.add(op.id); } + handling = current; - final boolean shouldClose = (folder == null); + if (handling.size() > 0 && process) { + Log.i(Helper.TAG, folder.name + " operations=" + operations.size()); + executor.submit(new Runnable() { + @Override + public void run() { + try { + wlFolder.acquire(); + Log.i(Helper.TAG, folder.name + " process"); - try { - if (folder == null) - folder = db.folder().getFolder(fid); + // Get folder + EntityFolder ofolder = null; + IMAPFolder ifolder = null; + for (EntityFolder f : folders.keySet()) + if (f.id == folder.id) { + ofolder = f; + ifolder = folders.get(f); + break; + } - Log.i(Helper.TAG, folder.name + " run " + (shouldClose ? "offline" : "online")); + final boolean shouldClose = (ofolder == null); - if (ifolder == null) { - // Prevent unnecessary folder connections - if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction())) - if (db.operation().getOperationCount(fid) == 0) - return; + try { + if (ofolder == null) + ofolder = db.folder().getFolder(folder.id); - db.folder().setFolderState(folder.id, "connecting"); + Log.i(Helper.TAG, ofolder.name + " run " + (shouldClose ? "offline" : "online")); - ifolder = (IMAPFolder) istore.getFolder(folder.name); - ifolder.open(Folder.READ_WRITE); + if (ifolder == null) { + // Prevent unnecessary folder connections + if (db.operation().getOperationCount(ofolder.id, null) == 0) + return; - db.folder().setFolderState(folder.id, "connected"); - db.folder().setFolderError(folder.id, null); - } + db.folder().setFolderState(ofolder.id, "connecting"); - if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction())) - processOperations(folder, isession, istore, ifolder, state); + ifolder = (IMAPFolder) istore.getFolder(ofolder.name); + ifolder.open(Folder.READ_WRITE); - else if (ACTION_SYNCHRONIZE_FOLDER.equals(intent.getAction())) { - processOperations(folder, isession, istore, ifolder, state); - synchronizeMessages(account, folder, ifolder, state); - } + db.folder().setFolderState(ofolder.id, "connected"); + db.folder().setFolderError(ofolder.id, null); + } - } 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 { - if (shouldClose) { - if (ifolder != null && ifolder.isOpen()) { - db.folder().setFolderState(folder.id, "closing"); - try { - ifolder.close(false); - } catch (MessagingException ex) { - Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + processOperations(account, ofolder, isession, istore, ifolder, state); + + } catch (Throwable 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(ofolder.id, "closing"); + try { + ifolder.close(false); + } catch (MessagingException ex) { + Log.w(Helper.TAG, ofolder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); + } + } + db.folder().setFolderState(ofolder.id, null); + } + } + } finally { + wlFolder.release(); + } } - } - db.folder().setFolderState(folder.id, null); + }); } } - } finally { - wl.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 BroadcastReceiver alarm = new BroadcastReceiver() { @@ -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); - db.message().setMessageError(message.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 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); + Handler handler = null; + final EntityFolder outbox = db.folder().getOutbox(); + if (outbox != null) { + db.folder().setFolderError(outbox.id, null); - db.folder().setFolderState(outbox.id, "connected"); - db.folder().setFolderError(outbox.id, null); + handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(android.os.Message msg) { + Log.i(Helper.TAG, outbox.name + " observe=" + msg.what); - lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS) - .setType("account/outbox") - .putExtra("folder", outbox.id)); + if (msg.what == 0) + db.operation().liveOperations(outbox.id).removeObservers(ServiceSynchronize.this); + else { + db.operation().liveOperations(outbox.id).observe(ServiceSynchronize.this, new Observer>() { + private List handling = new ArrayList<>(); + private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); + + @Override + public void onChanged(List operations) { + boolean process = false; + List 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 accounts = db.account().getAccounts(true); @@ -2463,9 +2476,11 @@ public class ServiceSynchronize extends LifecycleService { threadState.clear(); // Stop monitoring outbox - lbm.unregisterReceiver(outboxReceiver); - Log.i(Helper.TAG, outbox.name + " unlisten operations"); - db.folder().setFolderState(outbox.id, null); + 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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09b76babc2..4a7107ca44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -240,6 +240,7 @@ Report message as spam? Showing the original message can leak privacy sensitive information Showing images can leak privacy sensitive information + Synchronization will take place on next account connection Fix Compose