Added poll support

This commit is contained in:
M66B 2018-08-23 09:48:27 +00:00
parent 65a4a25101
commit 510bc3be43
15 changed files with 902 additions and 122 deletions

2
FAQ.md
View File

@ -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.
<a name="FAQ5"></a>
**(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.

View File

@ -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

View File

@ -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"
}

View File

@ -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\")"
]
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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);

View File

@ -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());
}

View File

@ -108,7 +108,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPort" />
<!-- password -->
<!-- user -->
<TextView
android:id="@+id/tvUser"
@ -185,6 +185,25 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbPrimary" />
<TextView
android:id="@+id/tvInterval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_poll_interval"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbStoreSent" />
<EditText
android:id="@+id/etInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInterval" />
<!-- check -->
<Button
@ -194,7 +213,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_check"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbStoreSent" />
app:layout_constraintTop_toBottomOf="@id/etInterval" />
<ProgressBar
android:id="@+id/pbCheck"
@ -214,7 +233,19 @@
android:layout_marginTop="12dp"
android:src="@drawable/baseline_delete_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbStoreSent" />
app:layout_constraintTop_toBottomOf="@id/etInterval" />
<TextView
android:id="@+id/tvIdle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minWidth="100dp"
android:text="@string/title_no_idle"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnCheck" />
<TextView
android:id="@+id/tvDrafts"
@ -234,7 +265,7 @@
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvDrafts"
app:layout_constraintTop_toBottomOf="@id/btnCheck" />
app:layout_constraintTop_toBottomOf="@id/tvIdle" />
<TextView
android:id="@+id/tvSent"

View File

@ -6,7 +6,7 @@
<ProgressBar
android:id="@+id/progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="50"

View File

@ -82,7 +82,7 @@
<ProgressBar
android:id="@+id/progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="50"

View File

@ -85,6 +85,7 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_poll_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>

View File

@ -6,7 +6,7 @@
<attr name="colorDrawerBackground" format="reference" />
<attr name="drawableItemBackground" format="reference" />
<style name="AppThemeLight" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppThemeLight" parent="Base.Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowDisablePreview">true</item>
<item name="colorPrimary">@color/colorPrimary</item>
@ -22,7 +22,7 @@
<item name="drawableItemBackground">@drawable/item_background_light</item>
</style>
<style name="AppThemeDark" parent="Theme.AppCompat">
<style name="AppThemeDark" parent="Base.Theme.AppCompat">
<item name="android:windowDisablePreview">true</item>
<item name="colorPrimary">@color/colorPrimary</item>
@ -38,7 +38,7 @@
<item name="drawableItemBackground">@drawable/item_background_dark</item>
</style>
<style name="Theme.Transparent" parent="Theme.AppCompat">
<style name="Theme.Transparent" parent="Base.Theme.AppCompat">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>

View File

@ -11,8 +11,8 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
google()
}
}