diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index cfc40c5949..c804cf4299 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 ce78f33a90..645693cd85 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 ca6bcfea64..7951643c0b 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 06a49597b1..5f54d54e83 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 0000000000..25610dc4cb --- /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 76a008e3fe..3692bd954d 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 d7c8b0cbf7..f7bf7878d7 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 4f3f7f50a6..2f7ec3b6c9 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 87d9d72df4..649a9bcd56 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 517c870ea4..77eb8071ae 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" /> - + + + + +