From e3e0f58197f56e59d8e2d607e39506cde7798347 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 7 Jan 2019 15:05:24 +0000 Subject: [PATCH] Added snoozing messages --- FAQ.md | 2 +- README.md | 1 + app/schemas/eu.faircode.email.DB/32.json | 1302 +++++++++++++++++ .../eu/faircode/email/AdapterMessage.java | 17 +- app/src/main/java/eu/faircode/email/DB.java | 10 +- .../java/eu/faircode/email/DaoMessage.java | 13 +- .../java/eu/faircode/email/EntityMessage.java | 26 +- .../eu/faircode/email/FragmentMessages.java | 125 +- .../eu/faircode/email/ReceiverAutostart.java | 20 + .../eu/faircode/email/ServiceSynchronize.java | 9 +- .../res/drawable/baseline_timelapse_24.xml | 10 + app/src/main/res/layout/dialog_duration.xml | 49 + app/src/main/res/layout/fragment_legend.xml | 23 +- .../main/res/layout/item_message_compact.xml | 12 +- .../main/res/layout/item_message_normal.xml | 12 +- app/src/main/res/menu/menu_messages.xml | 6 + app/src/main/res/values/strings.xml | 5 + 17 files changed, 1622 insertions(+), 20 deletions(-) create mode 100644 app/schemas/eu.faircode.email.DB/32.json create mode 100644 app/src/main/res/drawable/baseline_timelapse_24.xml create mode 100644 app/src/main/res/layout/dialog_duration.xml diff --git a/FAQ.md b/FAQ.md index b0bd911182..c7757f3064 100644 --- a/FAQ.md +++ b/FAQ.md @@ -41,7 +41,7 @@ None at this moment. * Resize images: this is not a feature directly related to email and there are plenty of apps that can do this for you. * Calendar events: opening the attached calendar file should open the related calendar app. * Executing filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for this. -* Snooze/send timer: basically the same as executing filter rules. Snoozing and delayed sending is not supported by [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol). You could move messages to a "to do" folder instead. +* Send timer: basically the same as executing filter rules. Delayed sending is not supported by [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol). You could move messages to a "to do" folder instead. * Badge count: there is no standard Android API for this and third party solutions might stop working anytime. For example *ShortcutBadger* [has lots of problems](https://github.com/leolin310148/ShortcutBadger/issues). You can use the provided widget instead. * 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. * Select identities to show in unified inbox: this would add complexity for something which would hardly be used. diff --git a/README.md b/README.md index f33cd2f9a6..dfd5f60508 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This app starts a foreground service with a low priority status bar notification * Account/identity colors * Notifications per account * Notifications with message preview (requires Android 7 Nougat or later) +* Snoozing messages * Reply templates * Search on server * Keyword management diff --git a/app/schemas/eu.faircode.email.DB/32.json b/app/schemas/eu.faircode.email.DB/32.json new file mode 100644 index 0000000000..6288f46cef --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/32.json @@ -0,0 +1,1302 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "9c9272252c9dbee760c5dca891e1a0b4", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `account` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `signature` TEXT, `auth_type` 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, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `replyto` TEXT, `bcc` TEXT, `delivery_receipt` INTEGER NOT NULL, `read_receipt` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `tbd` 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": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "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": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "delivery_receipt", + "columnName": "delivery_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read_receipt", + "columnName": "read_receipt", + "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": "tbd", + "columnName": "tbd", + "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, `auth_type` 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, `name` TEXT, `signature` TEXT, `color` INTEGER, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `browse` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `prefix` TEXT, `created` INTEGER, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "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": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browse", + "columnName": "browse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "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, `level` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `poll` INTEGER NOT NULL, `download` INTEGER NOT NULL, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `display` TEXT, `hide` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `keywords` TEXT, `initialize` INTEGER NOT NULL, `tbc` INTEGER, `tbd` INTEGER, `state` TEXT, `sync_state` TEXT, `error` TEXT, `last_sync` INTEGER, 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": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "download", + "columnName": "download", + "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": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initialize", + "columnName": "initialize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tbc", + "columnName": "tbc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync_state", + "columnName": "sync_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_sync", + "columnName": "last_sync", + "affinity": "INTEGER", + "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, `sender` 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, `ui_snoozed` INTEGER, `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": "sender", + "columnName": "sender", + "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": "ui_snoozed", + "columnName": "ui_snoozed", + "affinity": "INTEGER", + "notNull": false + }, + { + "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", + "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_sender", + "unique": false, + "columnNames": [ + "sender" + ], + "createSql": "CREATE INDEX `index_message_sender` ON `${TABLE_NAME}` (`sender`)" + }, + { + "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_flagged", + "unique": false, + "columnNames": [ + "ui_flagged" + ], + "createSql": "CREATE INDEX `index_message_ui_flagged` ON `${TABLE_NAME}` (`ui_flagged`)" + }, + { + "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`)" + }, + { + "name": "index_message_ui_snoozed", + "unique": false, + "columnNames": [ + "ui_snoozed" + ], + "createSql": "CREATE INDEX `index_message_ui_snoozed` ON `${TABLE_NAME}` (`ui_snoozed`)" + } + ], + "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, `disposition` TEXT, `cid` TEXT, `encryption` INTEGER, `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": "disposition", + "columnName": "disposition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryption", + "columnName": "encryption", + "affinity": "INTEGER", + "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, \"9c9272252c9dbee760c5dca891e1a0b4\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 886d19b870..e99680b475 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -156,6 +156,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); + ivSnoozed.setVisibility(message.ui_snoozed == null ? View.GONE : View.VISIBLE); ivAnswered.setVisibility(message.ui_answered ? View.VISIBLE : View.GONE); ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE); btnDownloadAttachments.setVisibility(View.GONE); @@ -729,7 +736,9 @@ public class AdapterMessage extends RecyclerView.Adapter 0" + " ORDER BY" + @@ -73,7 +74,7 @@ public interface DaoMessage { " ELSE 0" + " END DESC, message.received DESC") @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - DataSource.Factory pagedUnifiedInbox(boolean threading, String sort, boolean debug); + DataSource.Factory pagedUnifiedInbox(boolean threading, String sort, boolean snoozed, boolean debug); String unseen_folder = "SUM(CASE WHEN message.ui_seen" + " OR (folder.id <> :folder AND folder.type = '" + EntityFolder.ARCHIVE + "')" + @@ -102,6 +103,7 @@ public interface DaoMessage { " JOIN folder f ON f.id = :folder" + " WHERE (message.account = f.account OR folder.type = '" + EntityFolder.OUTBOX + "')" + " AND (NOT message.ui_hide OR :debug)" + + " AND (:snoozed OR :found OR ui_snoozed IS NULL)" + " AND (NOT :found OR ui_found = :found)" + " GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" + " HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" + @@ -113,7 +115,7 @@ public interface DaoMessage { " ELSE 0" + " END DESC, message.received DESC") @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - DataSource.Factory pagedFolder(long folder, boolean threading, String sort, boolean found, boolean debug); + DataSource.Factory pagedFolder(long folder, boolean threading, String sort, boolean snoozed, boolean found, boolean debug); @Query("SELECT message.*" + ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + @@ -258,6 +260,9 @@ public interface DaoMessage { " AND NOT uid IS NULL") List getUids(long folder, Long received); + @Query("SELECT * FROM message WHERE NOT ui_snoozed IS NULL") + List getSnoozed(); + @Insert long insertMessage(EntityMessage message); @@ -319,6 +324,10 @@ public interface DaoMessage { @Query("UPDATE message SET ui_found = 0") int resetSearch(); + @Query("UPDATE message SET ui_snoozed = :wakeup" + + " WHERE id = :id") + int setMessageSnoozed(long id, Long wakeup); + @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 3b45511350..debffdac01 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -20,8 +20,11 @@ package eu.faircode.email; */ import android.Manifest; +import android.app.AlarmManager; +import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; @@ -81,7 +84,8 @@ import static androidx.room.ForeignKey.SET_NULL; @Index(value = {"ui_hide"}), @Index(value = {"ui_found"}), @Index(value = {"ui_ignored"}), - @Index(value = {"ui_browsed"}) + @Index(value = {"ui_browsed"}), + @Index(value = {"ui_snoozed"}) } ) public class EntityMessage implements Serializable { @@ -142,6 +146,7 @@ public class EntityMessage implements Serializable { public Boolean ui_ignored = false; @NonNull public Boolean ui_browsed = false; + public Long ui_snoozed; public String error; public Long last_attempt; // send @@ -276,6 +281,21 @@ public class EntityMessage implements Serializable { return false; } + static void snooze(Context context, long id, Long wakeup) { + Intent snoozed = new Intent(context, ServiceSynchronize.class); + snoozed.setAction("snooze:" + id); + PendingIntent pi = PendingIntent.getService(context, ServiceSynchronize.PI_SNOOZED, snoozed, PendingIntent.FLAG_UPDATE_CURRENT); + + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (wakeup == null) { + Log.i("Cancel snooze id=" + id); + am.cancel(pi); + } else { + Log.i("Set snooze id=" + id + " wakeup=" + new Date(wakeup)); + am.set(AlarmManager.RTC_WAKEUP, wakeup, pi); + } + } + public boolean uiEquals(Object obj) { if (obj instanceof EntityMessage) { EntityMessage other = (EntityMessage) obj; @@ -315,6 +335,8 @@ public class EntityMessage implements Serializable { this.ui_hide.equals(other.ui_hide) && this.ui_found.equals(other.ui_found) && this.ui_ignored.equals(other.ui_ignored) && + //this.ui_browsed.equals(other.ui_browsed) && + (this.ui_snoozed == null ? other.ui_snoozed == null : this.ui_snoozed.equals(other.ui_snoozed)) && (this.error == null ? other.error == null : this.error.equals(other.error))); } return false; @@ -359,6 +381,8 @@ public class EntityMessage implements Serializable { this.ui_hide.equals(other.ui_hide) && this.ui_found.equals(other.ui_found) && this.ui_ignored.equals(other.ui_ignored) && + this.ui_browsed.equals(other.ui_browsed) && + (this.ui_snoozed == null ? other.ui_snoozed == null : this.ui_snoozed.equals(other.ui_snoozed)) && (this.error == null ? other.error == null : this.error.equals(other.error))); } return false; diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index b2e321a3ba..f5c0585f10 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -44,6 +44,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageButton; +import android.widget.NumberPicker; import android.widget.TextView; import com.google.android.material.bottomnavigation.BottomNavigationView; @@ -52,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar; import java.io.Serializable; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -744,6 +746,7 @@ public class FragmentMessages extends FragmentEx { private final int action_delete = 7; private final int action_junk = 8; private final int action_move = 9; + private final int action_snooze = 10; @Override public void onClick(View v) { @@ -814,10 +817,12 @@ public class FragmentMessages extends FragmentEx { popupMenu.getMenu().add(Menu.NONE, action_trash, 6, R.string.title_trash); if (!result[8] && !result[9]) - popupMenu.getMenu().add(Menu.NONE, action_junk, 6, R.string.title_spam); + popupMenu.getMenu().add(Menu.NONE, action_junk, 7, R.string.title_spam); if (!result[9]) - popupMenu.getMenu().add(Menu.NONE, action_move, 7, R.string.title_move); + popupMenu.getMenu().add(Menu.NONE, action_move, 8, R.string.title_move); + + popupMenu.getMenu().add(Menu.NONE, action_snooze, 9, R.string.title_snooze); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override @@ -850,6 +855,9 @@ public class FragmentMessages extends FragmentEx { case action_move: onActionMove(); return true; + case action_snooze: + onActionSnooze(); + return true; default: return false; } @@ -1191,6 +1199,85 @@ public class FragmentMessages extends FragmentEx { } }.execute(FragmentMessages.this, args, "messages:move"); } + + private void onActionSnooze() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + + final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_duration, null); + final NumberPicker npHours = dview.findViewById(R.id.npHours); + final NumberPicker npDays = dview.findViewById(R.id.npDays); + + npHours.setMinValue(0); + npHours.setMaxValue(24); + + npDays.setMinValue(0); + npDays.setMaxValue(30); + + npHours.setValue(prefs.getInt("snooze_hours", 1)); + npDays.setValue(prefs.getInt("snooze_days", 0)); + + new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) + .setTitle(R.string.title_snooze) + .setView(dview) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + int hours = npHours.getValue(); + int days = npDays.getValue(); + long duration = (hours + days * 24) * 3600L * 1000L; + + if (duration > 0) { + prefs.edit().putInt("snooze_hours", hours).apply(); + prefs.edit().putInt("snooze_days", days).apply(); + } + + Bundle args = new Bundle(); + args.putLongArray("ids", getSelection()); + args.putLong("wakeup", duration == 0 ? 0 : new Date().getTime() + duration); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) { + long[] ids = args.getLongArray("ids"); + Long wakeup = args.getLong("wakeup"); + if (wakeup == 0) + wakeup = null; + + DB db = DB.getInstance(context); + for (long id : ids) { + EntityMessage message = db.message().getMessage(id); + if (message != null) { + List messages = db.message().getMessageByThread( + message.account, message.thread, threading ? null : id, null); + for (EntityMessage threaded : messages) { + db.message().setMessageSnoozed(threaded.id, wakeup); + EntityMessage.snooze(context, threaded.id, wakeup); + } + } + } + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); + } + }.execute(FragmentMessages.this, args, "messages:snooze"); + } catch (Throwable ex) { + Log.e(ex); + } + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + selectionTracker.clearSelection(); + } + }) + .show(); + } }); ((ActivityBase) getActivity()).addBackPressedListener(onBackPressedListener); @@ -1498,15 +1585,14 @@ public class FragmentMessages extends FragmentEx { @Override public void onPrepareOptionsMenu(Menu menu) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + menu.findItem(R.id.menu_search).setVisible( folder >= 0 && viewType != AdapterMessage.ViewType.SEARCH); + menu.findItem(R.id.menu_sort_on).setVisible( viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER); - menu.findItem(R.id.menu_folders).setVisible(primary >= 0); - menu.findItem(R.id.menu_folders).setIcon(connected ? R.drawable.baseline_folder_24 : R.drawable.baseline_folder_open_24); - menu.findItem(R.id.menu_move_sent).setVisible(outbox); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); String sort = prefs.getString("sort", "time"); if ("time".equals(sort)) menu.findItem(R.id.menu_sort_on_time).setChecked(true); @@ -1517,6 +1603,15 @@ public class FragmentMessages extends FragmentEx { else if ("sender".equals(sort)) menu.findItem(R.id.menu_sort_on_sender).setChecked(true); + menu.findItem(R.id.menu_folders).setVisible(primary >= 0); + menu.findItem(R.id.menu_folders).setIcon(connected ? R.drawable.baseline_folder_24 : R.drawable.baseline_folder_open_24); + + menu.findItem(R.id.menu_snoozed).setVisible(!outbox && + (viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER)); + menu.findItem(R.id.menu_snoozed).setChecked(prefs.getBoolean("snoozed", false)); + + menu.findItem(R.id.menu_move_sent).setVisible(outbox); + super.onPrepareOptionsMenu(menu); } @@ -1543,6 +1638,10 @@ public class FragmentMessages extends FragmentEx { onMenuSort("sender"); return true; + case R.id.menu_snoozed: + onMenuSnoozed(); + return true; + case R.id.menu_zoom: onMenuZoom(); return true; @@ -1567,6 +1666,13 @@ public class FragmentMessages extends FragmentEx { loadMessages(); } + private void onMenuSnoozed() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean snoozed = prefs.getBoolean("snoozed", false); + prefs.edit().putBoolean("snoozed", !snoozed).apply(); + loadMessages(); + } + private void onMenuZoom() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); int zoom = prefs.getInt("zoom", compact ? 0 : 1); @@ -1640,6 +1746,7 @@ public class FragmentMessages extends FragmentEx { // Observe folder/messages/search SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); String sort = prefs.getString("sort", "time"); + boolean snoozed = prefs.getBoolean("snoozed", false); boolean debug = prefs.getBoolean("debug", false); Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug); @@ -1651,7 +1758,7 @@ public class FragmentMessages extends FragmentEx { switch (viewType) { case UNIFIED: builder = new LivePagedListBuilder<>( - db.message().pagedUnifiedInbox(threading, sort, debug), LOCAL_PAGE_SIZE); + db.message().pagedUnifiedInbox(threading, sort, snoozed, debug), LOCAL_PAGE_SIZE); break; case FOLDER: @@ -1684,7 +1791,7 @@ public class FragmentMessages extends FragmentEx { .setPrefetchDistance(REMOTE_PAGE_SIZE) .build(); builder = new LivePagedListBuilder<>( - db.message().pagedFolder(folder, threading, sort, false, debug), configFolder); + db.message().pagedFolder(folder, threading, sort, snoozed, false, debug), configFolder); builder.setBoundaryCallback(searchCallback); break; @@ -1726,7 +1833,7 @@ public class FragmentMessages extends FragmentEx { .setPrefetchDistance(REMOTE_PAGE_SIZE) .build(); builder = new LivePagedListBuilder<>( - db.message().pagedFolder(folder, threading, "time", true, false), configSearch); + db.message().pagedFolder(folder, threading, "time", snoozed, true, false), configSearch); builder.setBoundaryCallback(searchCallback); break; } diff --git a/app/src/main/java/eu/faircode/email/ReceiverAutostart.java b/app/src/main/java/eu/faircode/email/ReceiverAutostart.java index a39ff52a14..5553910222 100644 --- a/app/src/main/java/eu/faircode/email/ReceiverAutostart.java +++ b/app/src/main/java/eu/faircode/email/ReceiverAutostart.java @@ -23,6 +23,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import java.util.List; + +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; + public class ReceiverAutostart extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { @@ -30,6 +34,22 @@ public class ReceiverAutostart extends BroadcastReceiver { Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { EntityLog.log(context, intent.getAction()); ServiceSynchronize.init(context); + + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + DB db = DB.getInstance(context); + List messages = db.message().getSnoozed(); + for (EntityMessage message : messages) + EntityMessage.snooze(context, message.id, message.ui_snoozed); + } catch (Throwable ex) { + Log.e(ex); + } + } + }); + thread.setPriority(THREAD_PRIORITY_BACKGROUND); + thread.start(); } } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 8a140ed2f5..1d15014381 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -157,6 +157,7 @@ public class ServiceSynchronize extends LifecycleService { static final int PI_ARCHIVE = 4; static final int PI_TRASH = 5; static final int PI_IGNORED = 6; + static final int PI_SNOOZED = 7; @Override public void onCreate() { @@ -355,6 +356,7 @@ public class ServiceSynchronize extends LifecycleService { case "archive": case "trash": case "ignore": + case "snooze": executor.submit(new Runnable() { @Override public void run() { @@ -388,6 +390,10 @@ public class ServiceSynchronize extends LifecycleService { db.message().setMessageUiIgnored(message.id, true); break; + case "snooze": + db.message().setMessageSnoozed(message.id, null); + break; + default: Log.w("Unknown action: " + parts[0]); } @@ -1153,6 +1159,7 @@ public class ServiceSynchronize extends LifecycleService { } } }, "idler." + folder.id); + idler.setPriority(THREAD_PRIORITY_BACKGROUND); idler.start(); idlers.add(idler); @@ -2953,6 +2960,7 @@ public class ServiceSynchronize extends LifecycleService { void runnable(Runnable runnable, String name) { thread = new Thread(runnable, name); + thread.setPriority(THREAD_PRIORITY_BACKGROUND); } void release() { @@ -2982,7 +2990,6 @@ public class ServiceSynchronize extends LifecycleService { } void start() { - thread.setPriority(THREAD_PRIORITY_BACKGROUND); thread.start(); yield(); } diff --git a/app/src/main/res/drawable/baseline_timelapse_24.xml b/app/src/main/res/drawable/baseline_timelapse_24.xml new file mode 100644 index 0000000000..89535a68b4 --- /dev/null +++ b/app/src/main/res/drawable/baseline_timelapse_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_duration.xml b/app/src/main/res/layout/dialog_duration.xml new file mode 100644 index 0000000000..877524265c --- /dev/null +++ b/app/src/main/res/layout/dialog_duration.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_legend.xml b/app/src/main/res/layout/fragment_legend.xml index c0044005fe..475a320130 100644 --- a/app/src/main/res/layout/fragment_legend.xml +++ b/app/src/main/res/layout/fragment_legend.xml @@ -197,6 +197,27 @@ app:layout_constraintStart_toEndOf="@id/ivCC" app:layout_constraintTop_toTopOf="@id/ivCC" /> + + + + + app:layout_constraintTop_toBottomOf="@id/ivSnoozed" /> + + + + + + More Spam Move + Snooze Archive Reply Moving to %1$s @@ -352,6 +353,7 @@ Sent: Unsent: Invalid: + Snoozed Move to sent Previous @@ -377,6 +379,7 @@ Conversation CC/BCC Attachment + Snoozed Draft/edit Answered Contacts @@ -418,6 +421,8 @@ Undo Add Browse + Hours + Days Report Do not ask this again