From 7e9ab05affa56c879f200300e4b6655811355727 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 24 Nov 2018 09:26:34 +0100 Subject: [PATCH] Added account option to individually notify new messages --- FAQ.md | 1 - app/schemas/eu.faircode.email.DB/6.json | 1110 +++++++++++++++++ app/src/main/java/eu/faircode/email/DB.java | 9 +- .../java/eu/faircode/email/DaoMessage.java | 10 +- .../java/eu/faircode/email/EntityAccount.java | 6 + .../eu/faircode/email/FragmentAccount.java | 6 + .../eu/faircode/email/ServiceSynchronize.java | 85 +- .../eu/faircode/email/TupleMessageEx.java | 2 + app/src/main/res/layout/fragment_account.xml | 13 +- app/src/main/res/values/strings.xml | 1 + 10 files changed, 1202 insertions(+), 41 deletions(-) create mode 100644 app/schemas/eu.faircode.email.DB/6.json diff --git a/FAQ.md b/FAQ.md index 70b7cd299e..bae9d8a3ab 100644 --- a/FAQ.md +++ b/FAQ.md @@ -48,7 +48,6 @@ Anything on this list is in random order and *might* be added in the near future * Shortcut frequently contacted: Android [doesn't support this anymore](https://developer.android.com/guide/topics/providers/contacts-provider#ObsoleteData). * Pull down to refresh: new messages are received in real-time, so manual refreshing is not needed, see also [this FAQ](#user-content-faq2). * Switch language: although it is possible to change the language of an app, Android is not designed for this. Better fix the translation in your language if needed, see [this FAQ](#user-content-faq26) about how to. -* Notification per account: this would result in multiple icons in the status bar and most people don't like this. Note that the account colors are shown in the notifications too. * Select identities to show in unified inbox: this would add complexity for something which would hardly be used. * Better design: please let me know what you have in mind [in this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168). * Hide archived messages: hiding archived messages which exists in other folders too would have a performance impact. 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 0000000000..8b8da91b6e --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/6.json @@ -0,0 +1,1110 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "cd352e378e3d3ee88a5c87dda68a4f1e", + "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, `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": "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, `flagged` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` 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": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "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": "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`)" + } + ], + "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 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": [] + }, + { + "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, \"cd352e378e3d3ee88a5c87dda68a4f1e\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java index 69a8ef7b35..2718eb45d0 100644 --- a/app/src/main/java/eu/faircode/email/DB.java +++ b/app/src/main/java/eu/faircode/email/DB.java @@ -46,7 +46,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory; // https://developer.android.com/topic/libraries/architecture/room.html @Database( - version = 5, + version = 6, entities = { EntityIdentity.class, EntityAccount.class, @@ -155,6 +155,13 @@ public abstract class DB extends RoomDatabase { db.execSQL("ALTER TABLE `message` ADD COLUMN `last_attempt` INTEGER"); } }) + .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 `account` ADD COLUMN `notify` 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 19554ca4ae..4138ae66b6 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -37,7 +37,7 @@ public interface DaoMessage { // https://www.sqlite.org/lang_select.html @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor" + + ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" + ", COUNT(message.id) AS count" + ", SUM(CASE WHEN message.ui_seen" + @@ -69,7 +69,7 @@ public interface DaoMessage { DataSource.Factory pagedUnifiedInbox(String sort, boolean debug); @Query("SELECT message.*" + - ", account.name AS accountName, identity.color AS accountColor" + + ", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" + ", COUNT(message.id) AS count" + ", SUM(CASE WHEN message.ui_seen" + @@ -102,7 +102,7 @@ public interface DaoMessage { DataSource.Factory pagedFolder(long folder, String sort, boolean found, boolean debug); @Query("SELECT message.*" + - ", account.name AS accountName, identity.color AS accountColor" + + ", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, 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" + ", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" + @@ -185,7 +185,7 @@ public interface DaoMessage { List getMessageWithoutPreview(); @Query("SELECT message.*" + - ", account.name AS accountName, identity.color AS accountColor" + + ", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, 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" + ", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" + @@ -200,7 +200,7 @@ public interface DaoMessage { LiveData liveMessage(long id); @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor" + + ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" + ", 1 AS count" + ", 1 AS unseen" + diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index 3d570ca11c..e4a1804522 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -58,6 +58,8 @@ public class EntityAccount { public Boolean primary; public Integer color; @NonNull + public Boolean notify; + @NonNull public Integer poll_interval; // keep-alive interval public Long created; public String state; @@ -78,6 +80,7 @@ public class EntityAccount { json.put("primary", primary); if (color != null) json.put("color", color); + json.put("notify", notify); json.put("poll_interval", poll_interval); // not created // not state @@ -101,6 +104,8 @@ public class EntityAccount { account.primary = json.getBoolean("primary"); if (json.has("color")) account.color = json.getInt("color"); + if (json.has("notify")) + account.notify = json.getBoolean("notify"); account.poll_interval = json.getInt("poll_interval"); return account; } @@ -120,6 +125,7 @@ public class EntityAccount { this.synchronize.equals(other.synchronize) && this.primary.equals(other.primary) && (this.color == null ? other.color == null : this.color.equals(other.color)) && + this.notify.equals(other.notify) && this.poll_interval.equals(other.poll_interval) && (this.created == null ? other.created == null : this.created.equals(other.created)) && (this.state == null ? other.state == null : this.state.equals(other.state)) && diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index ad2b0c8a8d..45bd41976e 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -111,6 +111,7 @@ public class FragmentAccount extends FragmentEx { private Button btnColor; private View vwColor; private ImageView ibColorDefault; + private CheckBox cbNotify; private CheckBox cbSynchronize; private CheckBox cbPrimary; @@ -183,6 +184,7 @@ public class FragmentAccount extends FragmentEx { btnColor = view.findViewById(R.id.btnColor); vwColor = view.findViewById(R.id.vwColor); ibColorDefault = view.findViewById(R.id.ibColorDefault); + cbNotify = view.findViewById(R.id.cbNotify); cbSynchronize = view.findViewById(R.id.cbSynchronize); cbPrimary = view.findViewById(R.id.cbPrimary); @@ -599,6 +601,7 @@ public class FragmentAccount extends FragmentEx { args.putString("name", etName.getText().toString()); args.putInt("color", color); + args.putBoolean("notify", cbNotify.isChecked()); args.putBoolean("synchronize", cbSynchronize.isChecked()); args.putBoolean("primary", cbPrimary.isChecked()); @@ -624,6 +627,7 @@ public class FragmentAccount extends FragmentEx { String name = args.getString("name"); Integer color = args.getInt("color"); + boolean notify = args.getBoolean("notify"); boolean synchronize = args.getBoolean("synchronize"); boolean primary = args.getBoolean("primary"); @@ -706,6 +710,7 @@ public class FragmentAccount extends FragmentEx { account.name = name; account.color = color; + account.notify = notify; account.synchronize = synchronize; account.primary = (account.synchronize && primary); @@ -954,6 +959,7 @@ public class FragmentAccount extends FragmentEx { tilPassword.getEditText().setText(account == null ? null : account.password); etName.setText(account == null ? null : account.name); + cbNotify.setChecked(account == null ? false : account.notify); cbSynchronize.setChecked(account == null ? true : account.synchronize); cbPrimary.setChecked(account == null ? true : account.primary); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index f1dbd73947..b889503bfc 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -48,6 +48,7 @@ import android.provider.ContactsContract; import android.text.Html; import android.text.TextUtils; import android.util.Log; +import android.util.LongSparseArray; import com.sun.mail.iap.ConnectionException; import com.sun.mail.imap.AppendUID; @@ -173,40 +174,60 @@ public class ServiceSynchronize extends LifecycleService { }); db.message().liveUnseenUnified().observe(this, new Observer>() { - private List notifying = new ArrayList<>(); + private LongSparseArray> notifying = new LongSparseArray<>(); @Override public void onChanged(List messages) { NotificationManager nm = getSystemService(NotificationManager.class); - List notifications = getNotificationUnseen(messages); - - List all = new ArrayList<>(); - List added = new ArrayList<>(); - List removed = new ArrayList<>(notifying); - for (Notification notification : notifications) { - Integer id = (int) notification.extras.getLong("id", 0); - if (id > 0) { - all.add(id); - if (removed.contains(id)) - removed.remove(id); - else - added.add(id); - } - } - if (notifications.size() == 0) - nm.cancel("unseen", 0); + Widget.update(ServiceSynchronize.this, messages.size()); + + LongSparseArray> accountMessages = new LongSparseArray<>(); - for (Integer id : removed) - nm.cancel("unseen", id); + for (int i = 0; i < notifying.size(); i++) + accountMessages.put(notifying.keyAt(i), new ArrayList()); - for (Notification notification : notifications) { - Integer id = (int) notification.extras.getLong("id", 0); - if ((id == 0 && added.size() + removed.size() > 0) || added.contains(id)) - nm.notify("unseen", id, notification); + for (TupleMessageEx message : messages) { + long account = (message.accountNotify ? message.account : 0); + if (accountMessages.indexOfKey(account) < 0) + accountMessages.put(account, new ArrayList()); + accountMessages.get(account).add(message); + if (notifying.indexOfKey(account) < 0) + notifying.put(account, new ArrayList()); } - notifying = all; + for (int i = 0; i < accountMessages.size(); i++) { + long account = accountMessages.keyAt(i); + List notifications = getNotificationUnseen(account, accountMessages.get(account)); + + List all = new ArrayList<>(); + List added = new ArrayList<>(); + List removed = notifying.get(account); + for (Notification notification : notifications) { + Integer id = (int) notification.extras.getLong("id", 0); + if (id > 0) { + all.add(id); + if (removed.contains(id)) + removed.remove(id); + else + added.add(id); + } + } + + if (notifications.size() == 0) + nm.cancel("unseen:" + account, 0); + + for (Integer id : removed) + nm.cancel("unseen:" + account, id); + + for (Notification notification : notifications) { + Integer id = (int) notification.extras.getLong("id", 0); + if ((id == 0 && added.size() + removed.size() > 0) || added.contains(id)) + nm.notify("unseen:" + account, id, notification); + } + + notifying.put(account, all); + } } }); } @@ -264,7 +285,7 @@ public class ServiceSynchronize extends LifecycleService { else if ("clear".equals(action)) { new SimpleTask() { @Override - protected Void onLoad(Context context, Bundle args) throws Throwable { + protected Void onLoad(Context context, Bundle args) { DB.getInstance(context).message().ignoreAll(); return null; } @@ -370,18 +391,18 @@ public class ServiceSynchronize extends LifecycleService { return builder; } - private List getNotificationUnseen(List messages) { - // https://developer.android.com/training/notify-user/group + private List getNotificationUnseen(long account, List messages) { List notifications = new ArrayList<>(); - Widget.update(this, messages.size()); - if (messages.size() == 0) return notifications; boolean pro = Helper.isPro(this); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + // https://developer.android.com/training/notify-user/group + String group = Long.toString(account); + // Build pending intent Intent view = new Intent(this, ActivityView.class); view.setAction("unified"); @@ -429,7 +450,7 @@ public class ServiceSynchronize extends LifecycleService { .setCategory(Notification.CATEGORY_STATUS) .setVisibility(Notification.VISIBILITY_PRIVATE) .setPublicVersion(pbuilder.build()) - .setGroup(BuildConfig.APPLICATION_ID) + .setGroup(group) .setGroupSummary(true); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) @@ -520,7 +541,7 @@ public class ServiceSynchronize extends LifecycleService { .setPriority(Notification.PRIORITY_DEFAULT) .setCategory(Notification.CATEGORY_MESSAGE) .setVisibility(Notification.VISIBILITY_PRIVATE) - .setGroup(BuildConfig.APPLICATION_ID) + .setGroup(group) .setGroupSummary(false) .addAction(actionSeen.build()) .addAction(actionArchive.build()) diff --git a/app/src/main/java/eu/faircode/email/TupleMessageEx.java b/app/src/main/java/eu/faircode/email/TupleMessageEx.java index 5a42810086..cb000029e7 100644 --- a/app/src/main/java/eu/faircode/email/TupleMessageEx.java +++ b/app/src/main/java/eu/faircode/email/TupleMessageEx.java @@ -22,6 +22,7 @@ package eu.faircode.email; public class TupleMessageEx extends EntityMessage { public String accountName; public Integer accountColor; + public boolean accountNotify; public String folderName; public String folderDisplay; public String folderType; @@ -38,6 +39,7 @@ public class TupleMessageEx extends EntityMessage { return (super.equals(obj) && (this.accountName == null ? other.accountName == null : this.accountName.equals(other.accountName)) && (this.accountColor == null ? other.accountColor == null : this.accountColor.equals(other.accountColor)) && + this.accountNotify == other.accountNotify && this.folderName.equals(other.folderName) && (this.folderDisplay == null ? other.folderDisplay == null : this.folderDisplay.equals(other.folderDisplay)) && this.folderType.equals(other.folderType) && diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 56a1a15ad2..250ccfb10a 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -289,6 +289,15 @@ app:layout_constraintStart_toEndOf="@id/vwColor" app:layout_constraintTop_toBottomOf="@id/etName" /> + + + app:layout_constraintTop_toBottomOf="@id/cbNotify" /> + app:constraint_referenced_ids="tvName,etName,btnColor,vwColor,ibColorDefault,cbNotify,cbSynchronize,cbPrimary,tvInterval,etInterval" /> Used to differentiate folders Signature text Color + Separate notifications Domain name Get settings Settings not found