From 131beadea92213eca85aa53f10eb59816ae40598 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 1 Sep 2018 16:34:16 +0000 Subject: [PATCH] Progressive search --- .idea/misc.xml | 2 +- app/schemas/eu.faircode.email.DB/6.json | 850 ++++++++++++++++++ .../java/eu/faircode/email/ActivityView.java | 1 + app/src/main/java/eu/faircode/email/DB.java | 9 +- .../java/eu/faircode/email/DaoMessage.java | 23 +- .../java/eu/faircode/email/EntityMessage.java | 4 +- .../java/eu/faircode/email/FragmentAbout.java | 1 + .../eu/faircode/email/FragmentCompose.java | 2 + .../eu/faircode/email/FragmentMessage.java | 134 ++- .../eu/faircode/email/FragmentMessages.java | 239 ++++- .../eu/faircode/email/SearchDataSource.java | 241 ----- .../eu/faircode/email/ServiceSynchronize.java | 13 +- 12 files changed, 1156 insertions(+), 363 deletions(-) create mode 100644 app/schemas/eu.faircode.email.DB/6.json delete mode 100644 app/src/main/java/eu/faircode/email/SearchDataSource.java diff --git a/.idea/misc.xml b/.idea/misc.xml index 77d47004..caad359b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -30,7 +30,7 @@ - + diff --git a/app/schemas/eu.faircode.email.DB/6.json b/app/schemas/eu.faircode.email.DB/6.json new file mode 100644 index 00000000..8302e5cf --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/6.json @@ -0,0 +1,850 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "e9ae946be1049502f01f8f30275abc2f", + "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, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `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": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "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": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "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`)" + } + ], + "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, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "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": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen_until", + "columnName": "seen_until", + "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": [], + "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, `after` INTEGER NOT NULL, `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": "after", + "columnName": "after", + "affinity": "INTEGER", + "notNull": true + }, + { + "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`)" + } + ], + "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, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, 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 CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`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": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "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": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "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": "subject", + "columnName": "subject", + "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": "ui_seen", + "columnName": "ui_seen", + "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": "error", + "columnName": "error", + "affinity": "TEXT", + "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_folder_uid", + "unique": true, + "columnNames": [ + "folder", + "uid" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)" + }, + { + "name": "index_message_msgid_folder", + "unique": true, + "columnNames": [ + "msgid", + "folder" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)" + }, + { + "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`)" + } + ], + "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": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "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, `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": "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`)" + } + ], + "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 NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, 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": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + } + ], + "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": [] + } + ], + "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, \"e9ae946be1049502f01f8f30275abc2f\")" + ] + } +} \ 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 91106e70..422cdc32 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -294,6 +294,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack draft.seen = false; draft.ui_seen = false; draft.ui_hide = false; + draft.ui_found = false; draft.id = db.message().insertMessage(draft); draft.write(context, body); } diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java index 222a7abd..24cdf8da 100644 --- a/app/src/main/java/eu/faircode/email/DB.java +++ b/app/src/main/java/eu/faircode/email/DB.java @@ -45,7 +45,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; // https://developer.android.com/topic/libraries/architecture/room.html @Database( - version = 5, + version = 6, entities = { EntityIdentity.class, EntityAccount.class, @@ -143,6 +143,13 @@ public abstract class DB extends RoomDatabase { db.execSQL("ALTER TABLE `identity` ADD COLUMN `auth_type` INTEGER NOT NULL DEFAULT 1"); } }) + .addMigrations(new Migration(5, 6) { + @Override + public void migrate(SupportSQLiteDatabase db) { + Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion); + db.execSQL("ALTER TABLE `message` ADD COLUMN `ui_found` INTEGER NOT NULL DEFAULT 0"); + } + }) .build(); } diff --git a/app/src/main/java/eu/faircode/email/DaoMessage.java b/app/src/main/java/eu/faircode/email/DaoMessage.java index b7ba86c1..48df35bf 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -45,6 +45,7 @@ public interface DaoMessage { " JOIN folder ON folder.id = message.folder" + " WHERE account.`synchronize`" + " AND (NOT message.ui_hide OR :debug)" + + " AND NOT ui_found" + " GROUP BY CASE WHEN message.thread IS NULL THEN message.id ELSE message.thread END" + " HAVING SUM(CASE WHEN folder.type = '" + EntityFolder.INBOX + "' THEN 1 ELSE 0 END) > 0" + " ORDER BY message.received DESC") @@ -60,10 +61,11 @@ public interface DaoMessage { " JOIN folder ON folder.id = message.folder" + " LEFT JOIN folder f ON f.id = :folder" + " WHERE (NOT message.ui_hide OR :debug)" + + " AND ui_found = :found" + " GROUP BY CASE WHEN message.thread IS NULL THEN message.id ELSE message.thread END" + " HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" + " ORDER BY message.received DESC, message.sent DESC") - DataSource.Factory pagedFolder(long folder, boolean debug); + DataSource.Factory pagedFolder(long folder, boolean found, boolean debug); @Query("SELECT message.*, account.name AS accountName, folder.name as folderName, folder.type as folderType" + ", 1 AS count" + @@ -73,6 +75,7 @@ public interface DaoMessage { " LEFT JOIN account ON account.id = message.account" + " JOIN folder ON folder.id = message.folder" + " WHERE (NOT message.ui_hide OR :debug)" + + " AND NOT ui_found" + " AND message.account = (SELECT m1.account FROM message m1 WHERE m1.id = :msgid)" + " AND message.thread = (SELECT m2.thread FROM message m2 WHERE m2.id = :msgid)" + " ORDER BY message.received DESC, message.sent DESC") @@ -107,6 +110,11 @@ public interface DaoMessage { " AND folder.type <> '" + EntityFolder.OUTBOX + "'") List getMessageByThread(long account, String thread); + @Query("SELECT id FROM message" + + " WHERE folder = :folder" + + " ORDER BY message.received DESC, message.sent DESC") + List getMessageIDs(long folder); + @Query("SELECT message.*, account.name AS accountName, folder.name as folderName, folder.type as folderType" + ", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" + ", (SELECT COUNT(m2.id) FROM message m2 WHERE m2.account = message.account AND m2.thread = message.thread AND NOT m2.ui_hide AND NOT m2.ui_seen) AS unseen" + @@ -123,11 +131,16 @@ public interface DaoMessage { " WHERE account.`synchronize`" + " AND folder.type = '" + EntityFolder.INBOX + "'" + " AND NOT message.ui_seen AND NOT message.ui_hide" + + " AND NOT ui_found" + " AND (account.seen_until IS NULL OR message.stored > account.seen_until)" + " ORDER BY message.received") LiveData> liveUnseenUnified(); - @Query("SELECT uid FROM message WHERE folder = :folder AND received >= :received AND NOT uid IS NULL") + @Query("SELECT uid FROM message" + + " WHERE folder = :folder" + + " AND received >= :received" + + " AND NOT uid IS NULL" + + " AND NOT ui_found") List getUids(long folder, long received); @Insert @@ -151,6 +164,12 @@ public interface DaoMessage { @Query("UPDATE message SET error = :error WHERE id = :id") int setMessageError(long id, String error); + @Query("UPDATE message SET ui_found = :found WHERE id = :id") + int setMessageFound(long id, boolean found); + + @Query("UPDATE message SET ui_found = 0 WHERE folder = :folder") + int resetFound(long folder); + @Query("DELETE FROM message WHERE id = :id") int deleteMessage(long id); diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index 0c472098..acd5b4f9 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -98,12 +98,12 @@ public class EntityMessage implements Serializable { public Boolean ui_seen; @NonNull public Boolean ui_hide; + @NonNull + public Boolean ui_found; public String error; @Ignore String body = null; - @Ignore - boolean virtual = false; static String generateMessageId() { StringBuffer sb = new StringBuffer(); diff --git a/app/src/main/java/eu/faircode/email/FragmentAbout.java b/app/src/main/java/eu/faircode/email/FragmentAbout.java index 8a84f099..bbcb0a7a 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAbout.java +++ b/app/src/main/java/eu/faircode/email/FragmentAbout.java @@ -104,6 +104,7 @@ public class FragmentAbout extends FragmentEx { draft.seen = false; draft.ui_seen = false; draft.ui_hide = false; + draft.ui_found = false; draft.id = db.message().insertMessage(draft); draft.write(context, body); diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 6997c50f..9eca04fd 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -845,6 +845,7 @@ public class FragmentCompose extends FragmentEx { draft.seen = false; draft.ui_seen = false; draft.ui_hide = false; + draft.ui_found = false; draft.id = db.message().insertMessage(draft); draft.write(context, body == null ? "" : body); @@ -1107,6 +1108,7 @@ public class FragmentCompose extends FragmentEx { draft.uid = null; draft.msgid = msgid; draft.ui_hide = false; + draft.ui_found = false; draft.id = db.message().insertMessage(draft); draft.write(getContext(), pbody); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessage.java b/app/src/main/java/eu/faircode/email/FragmentMessage.java index 389f24f0..bfc659dc 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessage.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessage.java @@ -467,78 +467,76 @@ public class FragmentMessage extends FragmentEx { DB db = DB.getInstance(getContext()); - if (!message.virtual) { - // Observe message - db.message().liveMessage(message.id).observe(getViewLifecycleOwner(), new Observer() { + // Observe message + db.message().liveMessage(message.id).observe(getViewLifecycleOwner(), new Observer() { - @Override - public void onChanged(@Nullable final TupleMessageEx message) { - if (message == null || (!(debug && BuildConfig.DEBUG) && message.ui_hide)) { - // Message gone (moved, deleted) - finish(); - return; - } - - // Messages are immutable except for flags - FragmentMessage.this.message.seen = message.seen; - FragmentMessage.this.message.ui_seen = message.ui_seen; - setSeen(); + @Override + public void onChanged(@Nullable final TupleMessageEx message) { + if (message == null || (!(debug && BuildConfig.DEBUG) && message.ui_hide)) { + // Message gone (moved, deleted) + finish(); + return; } - }); - // Observe attachments - db.attachment().liveAttachments(message.id).observe(getViewLifecycleOwner(), - new Observer>() { - @Override - public void onChanged(@Nullable List attachments) { - if (attachments == null) - attachments = new ArrayList<>(); + // Messages are immutable except for flags + FragmentMessage.this.message.seen = message.seen; + FragmentMessage.this.message.ui_seen = message.ui_seen; + setSeen(); + } + }); - adapter.set(attachments); - grpAttachments.setVisibility(!free && attachments.size() > 0 ? View.VISIBLE : View.GONE); - } - }); + // Observe attachments + db.attachment().liveAttachments(message.id).observe(getViewLifecycleOwner(), + new Observer>() { + @Override + public void onChanged(@Nullable List attachments) { + if (attachments == null) + attachments = new ArrayList<>(); - // Observe folders - db.folder().liveFolders(message.account).observe(getViewLifecycleOwner(), new Observer>() { - @Override - public void onChanged(@Nullable List folders) { - if (folders == null) - folders = new ArrayList<>(); - - boolean inInbox = EntityFolder.INBOX.equals(message.folderType); - boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType); - boolean inArchive = EntityFolder.ARCHIVE.equals(message.folderType); - boolean inTrash = EntityFolder.TRASH.equals(message.folderType); - boolean inJunk = EntityFolder.JUNK.equals(message.folderType); - - boolean hasTrash = false; - boolean hasJunk = false; - boolean hasArchive = false; - boolean hasUser = false; - if (folders != null) - for (EntityFolder folder : folders) { - if (EntityFolder.TRASH.equals(folder.type)) - hasTrash = true; - else if (EntityFolder.JUNK.equals(folder.type)) - hasJunk = true; - else if (EntityFolder.ARCHIVE.equals(folder.type)) - hasArchive = true; - else if (EntityFolder.USER.equals(folder.type)) - hasUser = true; - } + adapter.set(attachments); + grpAttachments.setVisibility(!free && attachments.size() > 0 ? View.VISIBLE : View.GONE); + } + }); - bottom_navigation.setTag(inTrash || !hasTrash || inOutbox); + // Observe folders + db.folder().liveFolders(message.account).observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(@Nullable List folders) { + if (folders == null) + folders = new ArrayList<>(); + + boolean inInbox = EntityFolder.INBOX.equals(message.folderType); + boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType); + boolean inArchive = EntityFolder.ARCHIVE.equals(message.folderType); + boolean inTrash = EntityFolder.TRASH.equals(message.folderType); + boolean inJunk = EntityFolder.JUNK.equals(message.folderType); + + boolean hasTrash = false; + boolean hasJunk = false; + boolean hasArchive = false; + boolean hasUser = false; + if (folders != null) + for (EntityFolder folder : folders) { + if (EntityFolder.TRASH.equals(folder.type)) + hasTrash = true; + else if (EntityFolder.JUNK.equals(folder.type)) + hasJunk = true; + else if (EntityFolder.ARCHIVE.equals(folder.type)) + hasArchive = true; + else if (EntityFolder.USER.equals(folder.type)) + hasUser = true; + } - bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.uid != null && !inArchive && !inJunk && hasJunk); - bottom_navigation.getMenu().findItem(R.id.action_trash).setVisible((message.uid != null && hasTrash) || (inOutbox && !TextUtils.isEmpty(message.error))); - bottom_navigation.getMenu().findItem(R.id.action_move).setVisible(message.uid != null && (!inInbox || hasUser)); - bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.uid != null && !inArchive && hasArchive); - bottom_navigation.getMenu().findItem(R.id.action_reply).setVisible(!inOutbox); - bottom_navigation.setVisibility(View.VISIBLE); - } - }); - } + bottom_navigation.setTag(inTrash || !hasTrash || inOutbox); + + bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.uid != null && !inArchive && !inJunk && hasJunk); + bottom_navigation.getMenu().findItem(R.id.action_trash).setVisible((message.uid != null && hasTrash) || (inOutbox && !TextUtils.isEmpty(message.error))); + bottom_navigation.getMenu().findItem(R.id.action_move).setVisible(message.uid != null && (!inInbox || hasUser)); + bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.uid != null && !inArchive && hasArchive); + bottom_navigation.getMenu().findItem(R.id.action_reply).setVisible(!inOutbox); + bottom_navigation.setVisibility(View.VISIBLE); + } + }); } private void setSeen() { @@ -567,9 +565,9 @@ public class FragmentMessage extends FragmentEx { boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType); menu.findItem(R.id.menu_addresses).setVisible(!free); - menu.findItem(R.id.menu_thread).setVisible(!free && !message.virtual && message.count > 1); - menu.findItem(R.id.menu_forward).setVisible(!free && !message.virtual && !inOutbox); - menu.findItem(R.id.menu_reply_all).setVisible(!free && !message.virtual && message.cc != null && !inOutbox); + menu.findItem(R.id.menu_thread).setVisible(!free && message.count > 1); + menu.findItem(R.id.menu_forward).setVisible(!free && !inOutbox); + menu.findItem(R.id.menu_reply_all).setVisible(!free && message.cc != null && !inOutbox); menu.findItem(R.id.menu_decrypt).setVisible(decrypted == null && !inOutbox); } diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index c2725e43..5f09de42 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -39,17 +39,38 @@ import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; +import com.sun.mail.imap.IMAPFolder; +import com.sun.mail.imap.IMAPMessage; +import com.sun.mail.imap.IMAPStore; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.search.AndTerm; +import javax.mail.search.BodyTerm; +import javax.mail.search.ComparisonTerm; +import javax.mail.search.FromStringTerm; +import javax.mail.search.OrTerm; +import javax.mail.search.ReceivedDateTerm; +import javax.mail.search.SubjectTerm; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.constraintlayout.widget.Group; import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.GenericLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; -import androidx.paging.DataSource; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; import androidx.recyclerview.widget.ItemTouchHelper; @@ -70,8 +91,6 @@ public class FragmentMessages extends FragmentEx { private long thread = -1; private String search = null; - private SearchDataSource sds = null; - private long primary = -1; private AdapterMessage adapter; @@ -294,60 +313,196 @@ public class FragmentMessages extends FragmentEx { } }); - messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, debug), MESSAGES_PAGE_SIZE).build(); + messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, false, debug), MESSAGES_PAGE_SIZE).build(); } else { setSubtitle(R.string.title_folder_thread); messages = new LivePagedListBuilder<>(db.message().pagedThread(thread, debug), MESSAGES_PAGE_SIZE).build(); } + + messages.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(@Nullable PagedList messages) { + if (messages == null) { + finish(); + return; + } + + Log.i(Helper.TAG, "Submit messages=" + messages.size()); + adapter.submitList(messages); + + pbWait.setVisibility(View.GONE); + grpReady.setVisibility(View.VISIBLE); + + if (messages.size() == 0) { + tvNoEmail.setVisibility(View.VISIBLE); + rvMessage.setVisibility(View.GONE); + } else { + tvNoEmail.setVisibility(View.GONE); + rvMessage.setVisibility(View.VISIBLE); + } + } + }); } else { setSubtitle(getString(R.string.title_searching, search)); - // Searching is expensive: - // - reuse existing data source - // - use fragment lifecycle (instead of getViewLifecycleOwner) - // - saving state is not feasible - if (sds == null) - sds = new SearchDataSource(getContext(), this, folder, search); - - messages = new LivePagedListBuilder<>( - new DataSource.Factory() { - @Override - public DataSource create() { - return sds; + Bundle args = new Bundle(); + args.putLong("folder", folder); + args.putString("search", search); + + new SimpleTask() { + @Override + protected Void onLoad(Context context, Bundle args) throws Throwable { + long folder = args.getLong("folder"); + String search = args.getString("search").toLowerCase(); + + db.message().resetFound(folder); + + for (long id : db.message().getMessageIDs(folder)) { + EntityMessage message = db.message().getMessage(id); + String from = MessageHelper.getFormattedAddresses(message.from, true); + if (from.toLowerCase().contains(search) || + message.subject.toLowerCase().contains(search) || + message.read(context).toLowerCase().contains(search)) { + Log.i(Helper.TAG, "SDS found id=" + id); + db.message().setMessageFound(message.id, true); } - }, - new PagedList.Config.Builder() - .setEnablePlaceholders(true) - .setInitialLoadSizeHint(SEARCH_PAGE_SIZE) - .setPageSize(SEARCH_PAGE_SIZE) - .build() - ).build(); - } + } - messages.observe(getViewLifecycleOwner(), new Observer>() { - @Override - public void onChanged(@Nullable PagedList messages) { - if (messages == null) { - finish(); - return; + return null; } - Log.i(Helper.TAG, "Submit messages=" + messages.size()); - adapter.submitList(messages); + @Override + protected void onLoaded(final Bundle args, Void data) { + LiveData> messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, true, false), SEARCH_PAGE_SIZE) + .setBoundaryCallback(new PagedList.BoundaryCallback() { + private IMAPStore istore = null; + private IMAPFolder ifolder = null; + private Message[] imessages = null; + private int offset = 0; + private boolean observing = false; + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Override + public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) { + final Context context = getContext(); + + if (!observing) { + observing = true; + getLifecycle().addObserver(new GenericLifecycleObserver() { + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_DESTROY) + new Thread(new Runnable() { + @Override + public void run() { + Log.i(Helper.TAG, "SDS close"); + try { + if (istore != null) + istore.close(); + } catch (Throwable ex) { + Log.i(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + } + } + }).start(); + } + }); + } + + executor.submit(new Runnable() { + @Override + public void run() { + try { + long folder = args.getLong("folder"); + String search = args.getString("search"); + + EntityFolder _folder = db.folder().getFolder(folder); + EntityAccount account = db.account().getAccount(_folder.account); + + // Refresh token + //if (account.auth_type == Helper.AUTH_TYPE_GMAIL) { + // account.password = Helper.refreshToken(context, "com.google", account.user, account.password); + // db.account().setAccountPassword(account.id, account.password); + //} + + if (imessages == null) { + Properties props = MessageHelper.getSessionProperties(context, account.auth_type); + props.setProperty("mail.imap.throwsearchexception", "true"); + Session isession = Session.getInstance(props, null); + + Log.i(Helper.TAG, "SDS connecting account=" + account.name); + istore = (IMAPStore) isession.getStore("imaps"); + istore.connect(account.host, account.port, account.user, account.password); + + Log.i(Helper.TAG, "SDS opening folder=" + _folder.name); + ifolder = (IMAPFolder) istore.getFolder(_folder.name); + ifolder.open(Folder.READ_WRITE); + + Log.i(Helper.TAG, "SDS searching=" + search + " before=" + new Date(itemAtEnd.received)); + imessages = ifolder.search( + new AndTerm( + new ReceivedDateTerm(ComparisonTerm.LT, new Date(itemAtEnd.received)), + new OrTerm( + new FromStringTerm(search), + new OrTerm( + new SubjectTerm(search), + new BodyTerm(search))))); + Log.i(Helper.TAG, "SDS found messages=" + imessages.length); + } + + Log.i(Helper.TAG, "SDS offset=" + offset); + List selected = new ArrayList<>(); + int index = imessages.length - 1 - offset; + while (selected.size() < SEARCH_PAGE_SIZE && index >= 0) { + if (imessages[index].getReceivedDate().getTime() < itemAtEnd.received) + selected.add(imessages[index]); + index--; + } + Log.i(Helper.TAG, "SDS selected messages=" + selected.size()); + + for (Message imessage : selected) { + Log.i(Helper.TAG, "Search sync uid=" + ifolder.getUID(imessage)); + ServiceSynchronize.synchronizeMessage(context, _folder, ifolder, (IMAPMessage) imessage, true); + } + + offset += selected.size(); + + Log.i(Helper.TAG, "SDS done"); + } catch (Throwable ex) { + Log.i(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + } + } + }); + } + }) + .build(); - pbWait.setVisibility(View.GONE); - grpReady.setVisibility(View.VISIBLE); + messages.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(@Nullable PagedList messages) { + if (messages == null) { + finish(); + return; + } - if (messages.size() == 0) { - tvNoEmail.setVisibility(View.VISIBLE); - rvMessage.setVisibility(View.GONE); - } else { - tvNoEmail.setVisibility(View.GONE); - rvMessage.setVisibility(View.VISIBLE); + Log.i(Helper.TAG, "Submit messages=" + messages.size()); + adapter.submitList(messages); + + pbWait.setVisibility(View.GONE); + grpReady.setVisibility(View.VISIBLE); + + if (messages.size() == 0) { + tvNoEmail.setVisibility(View.VISIBLE); + rvMessage.setVisibility(View.GONE); + } else { + tvNoEmail.setVisibility(View.GONE); + rvMessage.setVisibility(View.VISIBLE); + } + } + }); } - } - }); + }.load(FragmentMessages.this, args); + } Bundle args = new Bundle(); args.putLong("folder", folder); diff --git a/app/src/main/java/eu/faircode/email/SearchDataSource.java b/app/src/main/java/eu/faircode/email/SearchDataSource.java deleted file mode 100644 index f09b0361..00000000 --- a/app/src/main/java/eu/faircode/email/SearchDataSource.java +++ /dev/null @@ -1,241 +0,0 @@ -package eu.faircode.email; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.util.Log; -import android.util.SparseArray; -import android.widget.Toast; - -import com.sun.mail.imap.IMAPFolder; -import com.sun.mail.imap.IMAPStore; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import javax.mail.FetchProfile; -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.Session; -import javax.mail.UIDFolder; -import javax.mail.internet.MimeMessage; -import javax.mail.search.BodyTerm; -import javax.mail.search.FromStringTerm; -import javax.mail.search.OrTerm; -import javax.mail.search.SubjectTerm; - -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.OnLifecycleEvent; -import androidx.paging.PositionalDataSource; - -public class SearchDataSource extends PositionalDataSource implements LifecycleObserver { - private Context context; - private LifecycleOwner owner; - private long fid; - private String search; - - private EntityFolder folder; - private EntityAccount account; - private IMAPStore istore = null; - private IMAPFolder ifolder; - private Message[] imessages; - - private SparseArray cache = new SparseArray<>(); - - private static final float MAX_MEMORY_USAGE = 0.6f; // percent - - SearchDataSource(Context context, LifecycleOwner owner, long folder, String search) { - Log.i(Helper.TAG, "SDS create"); - - this.context = context; - this.owner = owner; - this.fid = folder; - this.search = search; - - owner.getLifecycle().addObserver(this); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - public void onDestroyed() { - Log.i(Helper.TAG, "SDS destroy"); - - new Thread(new Runnable() { - @Override - public void run() { - try { - if (istore != null) - istore.close(); - } catch (MessagingException ex) { - Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - } finally { - istore = null; - ifolder = null; - imessages = null; - cache.clear(); - } - } - }).start(); - - owner.getLifecycle().removeObserver(this); - } - - @Override - public void loadInitial(LoadInitialParams params, LoadInitialCallback callback) { - Log.i(Helper.TAG, "SDS load initial"); - try { - SearchResult result = search(search, params.requestedStartPosition, params.requestedLoadSize); - callback.onResult(result.messages, params.requestedStartPosition, result.total); - } catch (Throwable ex) { - Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - reportError(ex); - } - } - - @Override - public void loadRange(LoadRangeParams params, LoadRangeCallback callback) { - Log.i(Helper.TAG, "SDS load range"); - try { - SearchResult result = search(search, params.startPosition, params.loadSize); - callback.onResult(result.messages); - } catch (Throwable ex) { - Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - reportError(ex); - } - } - - private SearchResult search(String term, int from, int count) throws MessagingException, IOException { - Log.i(Helper.TAG, "SDS search from=" + from + " count=" + count); - - if (istore == null) { - DB db = DB.getInstance(context); - folder = db.folder().getFolder(fid); - account = db.account().getAccount(folder.account); - - // Refresh token - if (account.auth_type == Helper.AUTH_TYPE_GMAIL) { - account.password = Helper.refreshToken(context, "com.google", account.user, account.password); - db.account().setAccountPassword(account.id, account.password); - } - - Properties props = MessageHelper.getSessionProperties(context, account.auth_type); - props.setProperty("mail.imap.throwsearchexception", "true"); - Session isession = Session.getInstance(props, null); - - Log.i(Helper.TAG, "SDS connecting account=" + account.name); - istore = (IMAPStore) isession.getStore("imaps"); - istore.connect(account.host, account.port, account.user, account.password); - - Log.i(Helper.TAG, "SDS opening folder=" + folder.name); - ifolder = (IMAPFolder) istore.getFolder(folder.name); - ifolder.open(Folder.READ_WRITE); - - Log.i(Helper.TAG, "SDS searching term=" + term); - imessages = ifolder.search( - new OrTerm( - new FromStringTerm(term), - new OrTerm( - new SubjectTerm(term), - new BodyTerm(term)))); - Log.i(Helper.TAG, "SDS found messages=" + imessages.length); - } - - SearchResult result = new SearchResult(); - result.total = imessages.length; - result.messages = new ArrayList<>(); - - List selected = new ArrayList<>(); - int base = imessages.length - 1 - from; - for (int i = base; i >= 0 && i >= base - count + 1; i--) - selected.add(imessages[i]); - Log.i(Helper.TAG, "SDS selected messages=" + selected.size()); - - FetchProfile fp = new FetchProfile(); - fp.add(UIDFolder.FetchProfileItem.UID); - fp.add(IMAPFolder.FetchProfileItem.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - fp.add(FetchProfile.Item.CONTENT_INFO); - fp.add(IMAPFolder.FetchProfileItem.HEADERS); - fp.add(IMAPFolder.FetchProfileItem.MESSAGE); - ifolder.fetch(selected.toArray(new Message[0]), fp); - - for (int s = 0; s < selected.size(); s++) { - int pos = from + s; - if (cache.get(pos) != null) { - Log.i(Helper.TAG, "SDS from cache pos=" + pos); - result.messages.add(cache.get(pos)); - continue; - } - - Message imessage = selected.get(s); - - long uid = ifolder.getUID(imessage); - - MessageHelper helper = new MessageHelper((MimeMessage) imessage); - boolean seen = helper.getSeen(); - - TupleMessageEx message = new TupleMessageEx(); - message.id = uid; - message.account = folder.account; - message.folder = folder.id; - message.uid = uid; - message.msgid = helper.getMessageID(); - message.references = TextUtils.join(" ", helper.getReferences()); - message.inreplyto = helper.getInReplyTo(); - message.thread = helper.getThreadId(uid); - message.from = helper.getFrom(); - message.to = helper.getTo(); - message.cc = helper.getCc(); - message.bcc = helper.getBcc(); - message.reply = helper.getReply(); - message.subject = imessage.getSubject(); - message.received = imessage.getReceivedDate().getTime(); - message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime()); - message.seen = seen; - message.ui_seen = seen; - message.ui_hide = false; - - message.accountName = account.name; - message.folderName = folder.name; - message.folderType = folder.type; - message.count = 1; - message.unseen = (seen ? 0 : 1); - message.attachments = 0; - - message.body = helper.getHtml(); - message.virtual = true; - - result.messages.add(message); - - Runtime rt = Runtime.getRuntime(); - float used = (float) (rt.totalMemory() - rt.freeMemory()) / rt.maxMemory(); - if (used < MAX_MEMORY_USAGE) - cache.put(pos, message); - else - Log.i(Helper.TAG, "SDS memory used=" + used); - } - - Log.i(Helper.TAG, "SDS result=" + result.messages.size()); - return result; - } - - private void reportError(final Throwable ex) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - Toast.makeText(context, Helper.formatThrowable(ex), Toast.LENGTH_LONG).show(); - } - }); - } - - private class SearchResult { - int total; - List messages; - } -} diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index c62c715d..61182f6e 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -499,7 +499,7 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, folder.name + " messages added"); for (Message imessage : e.getMessages()) try { - synchronizeMessage(folder, ifolder, (IMAPMessage) imessage); + synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, false); } catch (MessageRemovedException ex) { Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } @@ -559,7 +559,7 @@ public class ServiceSynchronize extends LifecycleService { try { try { Log.i(Helper.TAG, folder.name + " message changed"); - synchronizeMessage(folder, ifolder, (IMAPMessage) e.getMessage()); + synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), false); } catch (MessageRemovedException ex) { Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } @@ -1237,7 +1237,7 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, folder.name + " add=" + imessages.length); for (int i = imessages.length - 1; i >= 0; i--) try { - int status = synchronizeMessage(folder, ifolder, (IMAPMessage) imessages[i]); + int status = synchronizeMessage(this, folder, ifolder, (IMAPMessage) imessages[i], false); if (status > 0) added++; else if (status < 0) @@ -1280,7 +1280,7 @@ public class ServiceSynchronize extends LifecycleService { } } - private int synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, IOException { + static int synchronizeMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, boolean found) throws MessagingException, IOException { long uid; try { FetchProfile fp = new FetchProfile(); @@ -1303,7 +1303,7 @@ public class ServiceSynchronize extends LifecycleService { MessageHelper helper = new MessageHelper(imessage); boolean seen = helper.getSeen(); - DB db = DB.getInstance(this); + DB db = DB.getInstance(context); try { int result = 0; @@ -1381,9 +1381,10 @@ public class ServiceSynchronize extends LifecycleService { message.seen = seen; message.ui_seen = seen; message.ui_hide = false; + message.ui_found = found; message.id = db.message().insertMessage(message); - message.write(this, helper.getHtml()); + message.write(context, helper.getHtml()); Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid); int sequence = 0;