diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser
index cfc40c59..c804cf42 100644
Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ
diff --git a/FAQ.md b/FAQ.md
index ce78f33a..645693cd 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -38,7 +38,7 @@ The low priority status bar notification shows the number of pending operations,
Valid security certificates are officially signed (not self signed) and have matching a host name.
-**(5) Why is IMAP IDLE required?**
+**(5) What does 'no IDLE support' mean?**
Without [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) emails need to be periodically fetched,
which is a waste of battery power and internet bandwidth and will delay notification of new emails.
diff --git a/README.md b/README.md
index ca6bcfea..7951643c 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ Secure
Efficient
---------
-* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) required (no Yahoo! support; no [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) support)
+* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) supported
* Built with latest development tools and libraries
* Android 6 Marshmallow or later required
diff --git a/app/build.gradle b/app/build.gradle
index 06a49597..5f54d54e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -45,27 +45,33 @@ repositories {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- def support_version = "1.0.0-alpha1"
- def androidx_version = "2.0.0-alpha1"
+ def androidx_version = "1.0.0-rc01"
+ def lifecycle_version = "2.0.0-beta01"
+ def room_version = "2.0.0-beta01"
+ def paging_version = "2.0.0-beta01"
def javamail_version = "1.6.0"
def jsoup_version = "1.11.3"
+ def jcharset_version = "2.0"
- // https://developer.android.com/topic/libraries/support-library/revisions.html
- // https://developer.android.com/topic/libraries/support-library/packages
+ // https://developer.android.com/topic/libraries/support-library/androidx-rn
// https://developer.android.com/topic/libraries/support-library/refactor
- implementation "androidx.appcompat:appcompat:$support_version"
- implementation "androidx.recyclerview:recyclerview:$support_version"
- implementation "com.google.android.material:material:$support_version"
- implementation "androidx.browser:browser:$support_version"
+ implementation "androidx.appcompat:appcompat:$androidx_version"
+ implementation "androidx.recyclerview:recyclerview:$androidx_version"
+ implementation "com.google.android.material:material:$androidx_version"
+ implementation "androidx.browser:browser:$androidx_version"
+ // https://mvnrepository.com/artifact/androidx.constraintlayout/constraintlayout
implementation "androidx.constraintlayout:constraintlayout:1.1.2"
// https://developer.android.com/topic/libraries/architecture/adding-components.html
- implementation "androidx.lifecycle:lifecycle-extensions:$androidx_version"
- implementation "androidx.room:room-runtime:$androidx_version"
- implementation "androidx.paging:paging-runtime:$androidx_version"
- annotationProcessor "androidx.lifecycle:lifecycle-compiler:$androidx_version"
- annotationProcessor "androidx.room:room-compiler:$androidx_version"
+ // https://developer.android.com/jetpack/docs/release-notes
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
+
+ implementation "androidx.room:room-runtime:$room_version"
+ annotationProcessor "androidx.room:room-compiler:$room_version"
+
+ implementation "androidx.paging:paging-runtime:$paging_version"
// https://javaee.github.io/javamail/
implementation "com.sun.mail:android-mail:$javamail_version"
@@ -75,5 +81,5 @@ dependencies {
implementation "org.jsoup:jsoup:$jsoup_version"
// http://www.freeutils.net/source/jcharset/
- implementation "net.freeutils:jcharset:2.0"
+ implementation "net.freeutils:jcharset:$jcharset_version"
}
diff --git a/app/schemas/eu.faircode.email.DB/2.json b/app/schemas/eu.faircode.email.DB/2.json
new file mode 100644
index 00000000..25610dc4
--- /dev/null
+++ b/app/schemas/eu.faircode.email.DB/2.json
@@ -0,0 +1,794 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "766abd8e0d6da78fb9c652a575c13a24",
+ "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, `primary` INTEGER NOT NULL, `synchronize` 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": "primary",
+ "columnName": "primary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "synchronize",
+ "columnName": "synchronize",
+ "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, `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": "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, `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": "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"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, \"766abd8e0d6da78fb9c652a575c13a24\")"
+ ]
+ }
+}
\ 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 76a008e3..3692bd95 100644
--- a/app/src/main/java/eu/faircode/email/DB.java
+++ b/app/src/main/java/eu/faircode/email/DB.java
@@ -44,7 +44,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
- version = 1,
+ version = 2,
entities = {
EntityIdentity.class,
EntityAccount.class,
@@ -92,87 +92,16 @@ public abstract class DB extends RoomDatabase {
super.onOpen(db);
}
})
- //.addMigrations(MIGRATION_1_2)
- //.addMigrations(MIGRATION_2_3)
- //.addMigrations(MIGRATION_3_4)
- //.addMigrations(MIGRATION_4_5)
- //.addMigrations(MIGRATION_5_6)
- //.addMigrations(MIGRATION_6_7)
+ .addMigrations(new Migration(1, 2) {
+ @Override
+ public void migrate(SupportSQLiteDatabase db) {
+ Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
+ db.execSQL("ALTER TABLE `account` ADD COLUMN `poll_interval` INTEGER NOT NULL DEFAULT 9");
+ }
+ })
.build();
}
- private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
- @Override
- public void migrate(SupportSQLiteDatabase db) {
- Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
- db.execSQL("CREATE TABLE IF NOT EXISTS `attachment`" +
- " (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
- ", `message` INTEGER NOT NULL" +
- ", `sequence` INTEGER NOT NULL" +
- ", `type` TEXT NOT NULL, `name` TEXT" +
- ", `content` BLOB, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
- db.execSQL("CREATE INDEX `index_attachment_message` ON `attachment` (`message`)");
- db.execSQL("CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `attachment` (`message`, `sequence`)");
- }
- };
-
- private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
- @Override
- public void migrate(SupportSQLiteDatabase db) {
- Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
- db.execSQL("ALTER TABLE `attachment` ADD COLUMN `size` INTEGER");
- db.execSQL("ALTER TABLE `attachment` ADD COLUMN `progress` INTEGER");
- }
- };
-
- private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
- @Override
- public void migrate(SupportSQLiteDatabase db) {
- Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
- db.execSQL("CREATE INDEX `index_message_ui_seen` ON `message` (`ui_seen`)");
- }
- };
-
- private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
- @Override
- public void migrate(SupportSQLiteDatabase db) {
- Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
- db.execSQL("CREATE INDEX `index_message_ui_hide` ON `message` (`ui_hide`)");
- }
- };
-
- private static final Migration MIGRATION_5_6 = 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 `seen_until` INTEGER");
- }
- };
-
- private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
- @Override
- public void migrate(SupportSQLiteDatabase db) {
- Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
- // Recreate is sometimes causing problems with ROOM
- db.execSQL("DROP TABLE `identity`");
- db.execSQL("CREATE TABLE `identity`" +
- " (`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" +
- ", `primary` INTEGER NOT NULL" +
- ", `synchronize` INTEGER NOT NULL" +
- ", FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
- db.execSQL("CREATE INDEX `index_identity_account` ON `identity` (`account`)");
- }
- };
-
public static class Converters {
@TypeConverter
public static String[] fromStringArray(String value) {
diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java
index d7c8b0cb..f7bf7878 100644
--- a/app/src/main/java/eu/faircode/email/EntityAccount.java
+++ b/app/src/main/java/eu/faircode/email/EntityAccount.java
@@ -48,6 +48,8 @@ public class EntityAccount {
public Boolean synchronize;
@NonNull
public Boolean store_sent;
+ @NonNull
+ public Integer poll_interval;
public Long seen_until;
public String state;
public String error;
diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java
index 4f3f7f50..2f7ec3b6 100644
--- a/app/src/main/java/eu/faircode/email/FragmentAccount.java
+++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java
@@ -38,6 +38,7 @@ import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Spinner;
+import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.textfield.TextInputLayout;
@@ -72,8 +73,10 @@ public class FragmentAccount extends FragmentEx {
private CheckBox cbSynchronize;
private CheckBox cbPrimary;
private CheckBox cbStoreSent;
+ private EditText etInterval;
private Button btnCheck;
private ProgressBar pbCheck;
+ private TextView tvIdle;
private Spinner spDrafts;
private Spinner spSent;
private Spinner spAll;
@@ -106,8 +109,10 @@ public class FragmentAccount extends FragmentEx {
cbSynchronize = view.findViewById(R.id.cbSynchronize);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbStoreSent = view.findViewById(R.id.cbStoreSent);
+ etInterval = view.findViewById(R.id.etInterval);
btnCheck = view.findViewById(R.id.btnCheck);
pbCheck = view.findViewById(R.id.pbCheck);
+ tvIdle = view.findViewById(R.id.tvIdle);
spDrafts = view.findViewById(R.id.spDrafts);
spSent = view.findViewById(R.id.spSent);
spAll = view.findViewById(R.id.spAll);
@@ -197,12 +202,11 @@ public class FragmentAccount extends FragmentEx {
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(host, Integer.parseInt(port), user, password);
- if (!istore.hasCapability("IDLE"))
- throw new MessagingException(getContext().getString(R.string.title_no_idle));
-
if (!istore.hasCapability("UIDPLUS"))
throw new MessagingException(getContext().getString(R.string.title_no_uidplus));
+ args.putBoolean("idle", istore.hasCapability("IDLE"));
+
for (Folder ifolder : istore.getDefaultFolder().list("*")) {
String type = null;
@@ -264,6 +268,8 @@ public class FragmentAccount extends FragmentEx {
btnCheck.setEnabled(true);
pbCheck.setVisibility(View.GONE);
+ tvIdle.setVisibility(args.getBoolean("idle") ? View.GONE : View.VISIBLE);
+
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
@@ -368,6 +374,7 @@ public class FragmentAccount extends FragmentEx {
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
args.putBoolean("store_sent", cbStoreSent.isChecked());
+ args.putString("poll_interval", etInterval.getText().toString());
args.putParcelable("drafts", drafts);
args.putParcelable("sent", sent);
args.putParcelable("all", all);
@@ -385,6 +392,7 @@ public class FragmentAccount extends FragmentEx {
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
boolean store_sent = args.getBoolean("store_sent");
+ String poll_interval = args.getString("poll_interval");
EntityFolder drafts = args.getParcelable("drafts");
EntityFolder sent = args.getParcelable("sent");
EntityFolder all = args.getParcelable("all");
@@ -402,6 +410,9 @@ public class FragmentAccount extends FragmentEx {
if (synchronize && drafts == null)
throw new Throwable(getContext().getString(R.string.title_no_drafts));
+ if (TextUtils.isEmpty(poll_interval))
+ poll_interval = "9";
+
// Check IMAP server
if (synchronize) {
Session isession = Session.getInstance(MessageHelper.getSessionProperties(), null);
@@ -410,8 +421,8 @@ public class FragmentAccount extends FragmentEx {
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(host, Integer.parseInt(port), user, password);
- if (!istore.hasCapability("IDLE"))
- throw new MessagingException(getContext().getString(R.string.title_no_idle));
+ if (!istore.hasCapability("UIDPLUS"))
+ throw new MessagingException(getContext().getString(R.string.title_no_uidplus));
} finally {
if (istore != null)
istore.close();
@@ -437,6 +448,7 @@ public class FragmentAccount extends FragmentEx {
account.synchronize = synchronize;
account.primary = (account.synchronize && primary);
account.store_sent = store_sent;
+ account.poll_interval = Integer.parseInt(poll_interval);
if (!synchronize)
account.error = null;
@@ -570,6 +582,7 @@ public class FragmentAccount extends FragmentEx {
pbCheck.setVisibility(View.GONE);
btnSave.setVisibility(View.GONE);
pbSave.setVisibility(View.GONE);
+ tvIdle.setVisibility(View.GONE);
grpFolders.setVisibility(View.GONE);
ibDelete.setVisibility(View.GONE);
@@ -618,6 +631,7 @@ public class FragmentAccount extends FragmentEx {
cbSynchronize.setChecked(account == null ? true : account.synchronize);
cbPrimary.setChecked(account == null ? true : account.primary);
cbStoreSent.setChecked(account == null ? false : account.store_sent);
+ etInterval.setText(account == null ? "9" : Integer.toString(account.poll_interval));
} else {
int provider = savedInstanceState.getInt("provider");
spProvider.setTag(provider);
diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
index 87d9d72d..649a9bcd 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
@@ -116,7 +116,6 @@ public class ServiceSynchronize extends LifecycleService {
private static final int CONNECT_BACKOFF_START = 2; // seconds
private static final int CONNECT_BACKOFF_MAX = 128; // seconds
private static final long STORE_NOOP_INTERVAL = 9 * 60 * 1000L; // ms
- private static final long FOLDER_NOOP_INTERVAL = 9 * 60 * 1000L; // ms
private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS";
@@ -552,18 +551,22 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " start noop");
while (state.running && ifolder.isOpen()) {
- Log.i(Helper.TAG, folder.name + " request NOOP");
- ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
- public Object doCommand(IMAPProtocol p) throws ProtocolException {
- Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
- p.simpleCommand("NOOP", null);
- Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
- return null;
- }
- });
-
try {
- Thread.sleep(FOLDER_NOOP_INTERVAL);
+ Thread.sleep(account.poll_interval * 60 * 1000L);
+
+ if (istore.hasCapability("IDLE")) {
+ Log.i(Helper.TAG, folder.name + " request NOOP");
+ ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
+ public Object doCommand(IMAPProtocol p) throws ProtocolException {
+ Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
+ p.simpleCommand("NOOP", null);
+ Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
+ return null;
+ }
+ });
+ } else
+ synchronizeMessages(account, folder, ifolder, state);
+
} catch (InterruptedException ex) {
Log.w(Helper.TAG, folder.name + " noop " + ex.getMessage());
}
diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml
index 517c870e..77eb8071 100644
--- a/app/src/main/res/layout/fragment_account.xml
+++ b/app/src/main/res/layout/fragment_account.xml
@@ -108,7 +108,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPort" />
-
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/etInterval" />
+ app:layout_constraintTop_toBottomOf="@id/etInterval" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/tvIdle" />
User name
Password
Store sent messages (enable if needed only)
+ Poll/keep-alive interval (minutes)
Synchronize (receive messages)
Synchronize (send messages)
Primary (default account)
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d684a396..bb80ef77 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -6,7 +6,7 @@
-
-
-