Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8462c2845 | ||
|
|
70e0913331 | ||
|
|
bc9a26c2c7 | ||
|
|
7e89563b48 | ||
|
|
4ada1dba5d | ||
|
|
cf73512897 | ||
|
|
f31644894e | ||
|
|
0a1759b198 | ||
|
|
5f77582bec | ||
|
|
222a16b963 | ||
|
|
9f248a7b30 | ||
|
|
63e40513fb | ||
|
|
d32df01e25 | ||
|
|
8ac235791f | ||
|
|
dabf802d84 | ||
|
|
b130da7bc1 | ||
|
|
14efe62e91 | ||
|
|
bb4bed926a | ||
|
|
485ef3ff56 | ||
|
|
9fbc8f1900 | ||
|
|
58adbf64c3 | ||
|
|
74b6cb037d | ||
|
|
fb4f0f2f58 | ||
|
|
f67f822267 | ||
|
|
20d60fafe8 | ||
|
|
29ae761435 | ||
|
|
3d3bf4b4d3 | ||
|
|
d17ff4f188 | ||
|
|
b3742cd525 | ||
|
|
5d4c13ad7f | ||
|
|
774f9b3f36 | ||
|
|
bcf86385ae | ||
|
|
4d6c41674d | ||
|
|
a499808691 | ||
|
|
01375cc84e | ||
|
|
30a2b5ee31 | ||
|
|
a78fb6ba91 | ||
|
|
ffb85c5403 | ||
|
|
268f94e77e | ||
|
|
1e25a9f97c | ||
|
|
c37f5b934d | ||
|
|
2597b5e825 | ||
|
|
921b31abec | ||
|
|
bb63ef2cfa | ||
|
|
7feac257d1 | ||
|
|
8c1ad78caf | ||
|
|
1240b29404 | ||
|
|
7a3a226102 | ||
|
|
930fb92327 | ||
|
|
3fe434b60c | ||
|
|
539ee934ea | ||
|
|
0b83d1eb67 |
BIN
.idea/caches/build_file_checksums.ser
generated
33
FAQ.md
@@ -5,23 +5,30 @@ Frequently Asked Questions
|
||||
--------------------------
|
||||
|
||||
<a name="FAQ1"></a>
|
||||
**(1) Which email providers are supported?**
|
||||
|
||||
* Gmail
|
||||
* Outlook
|
||||
|
||||
<a name="FAQ2"></a>
|
||||
**(2) What is a valid security certificate?**
|
||||
|
||||
Valid security certificates are officially signed (not self signed) and have matching a host name.
|
||||
|
||||
<a name="FAQ3"></a>
|
||||
**(3) Which permissions are needed and why?**
|
||||
**(1) Which permissions are needed and why?**
|
||||
|
||||
* Full network access (INTERNET): to send and receive email
|
||||
* View network connections (ACCESS_NETWORK_STATE): to monitor internet connectivity changes
|
||||
* Run at startup (RECEIVE_BOOT_COMPLETED): to start monitoring on device start
|
||||
|
||||
<a name="FAQ2"></a>
|
||||
**(2) What are operations?**
|
||||
|
||||
The low priority status bar notification shows the number of pending operations, which can be:
|
||||
|
||||
* Mark message as seen/unseen in remote folder
|
||||
* Add message to remote folder
|
||||
* Move message to another remote folder
|
||||
* Delete message from remote folder
|
||||
* Send message
|
||||
* Download attachment
|
||||
|
||||
<a name="FAQ3"></a>
|
||||
**(3) What is a valid security certificate?**
|
||||
|
||||
Valid security certificates are officially signed (not self signed) and have matching a host name.
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
If you have another question, you can use [this forum](https://forum.xda-developers.com/).
|
||||
If you have another question, you can use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).
|
||||
|
||||
@@ -5,7 +5,7 @@ Simple, secure and efficient email app
|
||||
|
||||
This email app might be for you if your current email app:
|
||||
|
||||
* takes long to show messages
|
||||
* takes long to show or receive messages
|
||||
* can manage only one mailbox
|
||||
* cannot show related messages
|
||||
* cannot work offline
|
||||
@@ -47,8 +47,8 @@ Secure
|
||||
Efficient
|
||||
---------
|
||||
|
||||
* No [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) support
|
||||
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) required
|
||||
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) required (no [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) support)
|
||||
* Built with latest development tools and libraries
|
||||
* Android 6 Marshmallow or later required
|
||||
|
||||
This app starts a foreground service with a low priority status bar notification to make sure you'll never miss new email.
|
||||
|
||||
@@ -6,9 +6,15 @@ android {
|
||||
applicationId "eu.faircode.email"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 28
|
||||
versionCode 2
|
||||
versionName "0.2"
|
||||
versionCode 4
|
||||
versionName "0.4"
|
||||
archivesBaseName = "SafeEmail-v$versionName"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
4
app/proguard-rules.pro
vendored
@@ -20,9 +20,13 @@
|
||||
# hide the original source file name.
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
#App
|
||||
-keep class eu.faircode.email.**
|
||||
-keepnames class eu.faircode.email.** { *; }
|
||||
|
||||
#Support library
|
||||
-keep class android.support.v7.app.AppCompatViewInflater{ <init>(...); }
|
||||
|
||||
#JavaMail
|
||||
-dontshrink
|
||||
-keep class javax.** {*;}
|
||||
|
||||
639
app/schemas/eu.faircode.email.DB/2.json
Normal file
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "28de77abd152c62bbdcca05efc2a6f59",
|
||||
"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, `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)",
|
||||
"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": "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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, 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
|
||||
}
|
||||
],
|
||||
"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, `body` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, 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": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sent",
|
||||
"columnName": "sent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "received",
|
||||
"columnName": "received",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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_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`)"
|
||||
}
|
||||
],
|
||||
"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, `type` TEXT NOT NULL, `name` TEXT, `content` BLOB, 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": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"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, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT, 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": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "args",
|
||||
"columnName": "args",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_operation_message",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"message"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"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, \"28de77abd152c62bbdcca05efc2a6f59\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,14 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppThemeLight">
|
||||
|
||||
<!-- do not contact Google servers -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
|
||||
<activity android:name=".ActivitySetup" />
|
||||
|
||||
<activity android:name=".ActivityView">
|
||||
|
||||
@@ -28,7 +28,9 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
|
||||
static final int LOADER_COMPOSE_PUT = 2;
|
||||
static final int LOADER_COMPOSE_DELETE = 3;
|
||||
|
||||
static final int REQUEST_CONTACT = 1;
|
||||
static final int REQUEST_CONTACT_TO = 1;
|
||||
static final int REQUEST_CONTACT_CC = 2;
|
||||
static final int REQUEST_CONTACT_BCC = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -38,11 +40,13 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
|
||||
if (getSupportFragmentManager().getFragments().size() == 0) {
|
||||
FragmentCompose fragment = new FragmentCompose();
|
||||
Bundle args = getIntent().getExtras();
|
||||
if (args == null)
|
||||
args = new Bundle();
|
||||
|
||||
FragmentCompose fragment = new FragmentCompose();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("compose");
|
||||
fragmentTransaction.commit();
|
||||
|
||||
@@ -64,6 +64,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
|
||||
static final int LOADER_ACCOUNT_PUT = 1;
|
||||
static final int LOADER_IDENTITY_PUT = 2;
|
||||
static final int LOADER_FOLDER_PUT = 3;
|
||||
static final int LOADER_MESSAGES_INIT = 4;
|
||||
|
||||
static final int REQUEST_VIEW = 1;
|
||||
|
||||
@@ -76,6 +77,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
@@ -211,9 +213,10 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
|
||||
if (prefs.getBoolean("eula", false)) {
|
||||
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("folder", -1);
|
||||
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
@@ -256,9 +259,10 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
|
||||
}
|
||||
|
||||
private void onMenuUnified() {
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("folder", -1);
|
||||
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
@@ -320,6 +324,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
|
||||
draft.ui_hide = false;
|
||||
draft.id = db.message().insertMessage(draft);
|
||||
|
||||
EntityOperation.queue(ActivityView.this, draft, EntityOperation.ADD);
|
||||
|
||||
startActivity(new Intent(ActivityView.this, ActivityCompose.class)
|
||||
.putExtra("id", draft.id));
|
||||
}
|
||||
|
||||
199
app/src/main/java/eu/faircode/email/AdapterAttachment.java
Normal file
@@ -0,0 +1,199 @@
|
||||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of Safe email.
|
||||
|
||||
Safe email is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
NetGuard is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.util.DiffUtil;
|
||||
import android.support.v7.util.ListUpdateCallback;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.ViewHolder> {
|
||||
private Context context;
|
||||
private ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private List<EntityAttachment> all = new ArrayList<>();
|
||||
private List<EntityAttachment> filtered = new ArrayList<>();
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener {
|
||||
View itemView;
|
||||
TextView tvName;
|
||||
TextView tvSize;
|
||||
ImageView ivDownload;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.itemView = itemView;
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
tvSize = itemView.findViewById(R.id.tvSize);
|
||||
ivDownload = itemView.findViewById(R.id.ivDownload);
|
||||
}
|
||||
|
||||
private void wire() {
|
||||
itemView.setOnClickListener(this);
|
||||
ivDownload.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void unwire() {
|
||||
itemView.setOnClickListener(null);
|
||||
ivDownload.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final EntityAttachment attachment = filtered.get(getLayoutPosition());
|
||||
if (attachment != null && attachment.content == null)
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
EntityMessage message = DB.getInstance(context).message().getMessage(attachment.message);
|
||||
EntityOperation.queue(context, message, EntityOperation.ATTACHMENT, attachment.sequence);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AdapterAttachment(Context context) {
|
||||
this.context = context;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void set(List<EntityAttachment> attachments) {
|
||||
Log.i(Helper.TAG, "Set attachments=" + attachments.size());
|
||||
|
||||
Collections.sort(attachments, new Comparator<EntityAttachment>() {
|
||||
@Override
|
||||
public int compare(EntityAttachment a1, EntityAttachment a2) {
|
||||
return a1.sequence.compareTo(a2.sequence);
|
||||
}
|
||||
});
|
||||
|
||||
all.clear();
|
||||
all.addAll(attachments);
|
||||
|
||||
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
|
||||
|
||||
filtered.clear();
|
||||
filtered.addAll(all);
|
||||
|
||||
diff.dispatchUpdatesTo(new ListUpdateCallback() {
|
||||
@Override
|
||||
public void onInserted(int position, int count) {
|
||||
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(int position, int count) {
|
||||
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoved(int fromPosition, int toPosition) {
|
||||
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(int position, int count, Object payload) {
|
||||
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
|
||||
}
|
||||
});
|
||||
diff.dispatchUpdatesTo(AdapterAttachment.this);
|
||||
}
|
||||
|
||||
private class MessageDiffCallback extends DiffUtil.Callback {
|
||||
private List<EntityAttachment> prev;
|
||||
private List<EntityAttachment> next;
|
||||
|
||||
MessageDiffCallback(List<EntityAttachment> prev, List<EntityAttachment> next) {
|
||||
this.prev = prev;
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return prev.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return next.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
EntityAttachment a1 = prev.get(oldItemPosition);
|
||||
EntityAttachment a2 = next.get(newItemPosition);
|
||||
return a1.id.equals(a2.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
EntityAttachment a1 = prev.get(oldItemPosition);
|
||||
EntityAttachment a2 = next.get(newItemPosition);
|
||||
return a1.equals(a2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return filtered.get(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return filtered.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_attachment, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.unwire();
|
||||
|
||||
EntityAttachment attachment = filtered.get(position);
|
||||
holder.tvName.setText(attachment.name);
|
||||
holder.tvSize.setVisibility((attachment.content == null ? View.GONE : View.VISIBLE));
|
||||
holder.ivDownload.setVisibility((attachment.content == null ? View.VISIBLE : View.GONE));
|
||||
|
||||
if (attachment.content != null)
|
||||
holder.tvSize.setText(Helper.humanReadableByteCount(attachment.content.length, false));
|
||||
|
||||
holder.wire();
|
||||
}
|
||||
}
|
||||
@@ -119,15 +119,17 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
||||
Collections.sort(folders, new Comparator<TupleFolderEx>() {
|
||||
@Override
|
||||
public int compare(TupleFolderEx f1, TupleFolderEx f2) {
|
||||
if (f1.accountName == null)
|
||||
if (f2.accountName == null)
|
||||
return 0;
|
||||
int s = EntityFolder.isUser(f1.type).compareTo(EntityFolder.isUser(f2.type));
|
||||
if (s == 0) {
|
||||
int a = collator.compare(
|
||||
f1.accountName == null ? "" : f1.accountName,
|
||||
f2.accountName == null ? "" : f2.accountName);
|
||||
if (a == 0)
|
||||
return collator.compare(f1.name, f2.name);
|
||||
else
|
||||
return -1;
|
||||
else if (f2.accountName == null)
|
||||
return 1;
|
||||
else
|
||||
return collator.compare(f1.accountName, f2.accountName);
|
||||
return a;
|
||||
} else
|
||||
return s;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,13 +226,20 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
||||
holder.tvName.setText(context.getString(R.string.title_folder_unseen, name, folder.unseen));
|
||||
else
|
||||
holder.tvName.setText(name);
|
||||
|
||||
holder.tvName.setTypeface(null, folder.unseen > 0 ? Typeface.BOLD : Typeface.NORMAL);
|
||||
|
||||
holder.tvAfter.setText(Integer.toString(folder.after));
|
||||
holder.tvAfter.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
holder.ivSync.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
|
||||
holder.tvCount.setText(Integer.toString(folder.messages));
|
||||
holder.tvType.setText(folder.type);
|
||||
|
||||
int resid = context.getResources().getIdentifier(
|
||||
"title_folder_" + folder.type.toLowerCase(),
|
||||
"string",
|
||||
context.getPackageName());
|
||||
holder.tvType.setText(resid > 0 ? context.getString(resid) : folder.type);
|
||||
|
||||
holder.tvAccount.setText(folder.accountName);
|
||||
|
||||
holder.wire();
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -56,8 +57,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
||||
public class ViewHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener {
|
||||
View itemView;
|
||||
TextView tvAddress;
|
||||
TextView tvFrom;
|
||||
TextView tvTime;
|
||||
ImageView ivAttachments;
|
||||
TextView tvSubject;
|
||||
TextView tvCount;
|
||||
|
||||
@@ -65,8 +67,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
||||
super(itemView);
|
||||
|
||||
this.itemView = itemView;
|
||||
tvAddress = itemView.findViewById(R.id.tvAddress);
|
||||
tvFrom = itemView.findViewById(R.id.tvFrom);
|
||||
tvTime = itemView.findViewById(R.id.tvTime);
|
||||
ivAttachments = itemView.findViewById(R.id.ivAttachments);
|
||||
tvSubject = itemView.findViewById(R.id.tvSubject);
|
||||
tvCount = itemView.findViewById(R.id.tvCount);
|
||||
}
|
||||
@@ -86,22 +89,26 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (EntityFolder.TYPE_DRAFTS.equals(message.folderType))
|
||||
context.startActivity(
|
||||
new Intent(context, ActivityCompose.class)
|
||||
.putExtra("id", message.id));
|
||||
else {
|
||||
if (!message.seen && !message.ui_seen) {
|
||||
message.ui_seen = !message.ui_seen;
|
||||
DB.getInstance(context).message().updateMessage(message);
|
||||
EntityOperation.queue(context, message, EntityOperation.SEEN, message.ui_seen);
|
||||
}
|
||||
try {
|
||||
if (EntityFolder.TYPE_DRAFTS.equals(message.folderType))
|
||||
context.startActivity(
|
||||
new Intent(context, ActivityCompose.class)
|
||||
.putExtra("id", message.id));
|
||||
else {
|
||||
if (!message.seen && !message.ui_seen) {
|
||||
message.ui_seen = !message.ui_seen;
|
||||
DB.getInstance(context).message().updateMessage(message);
|
||||
EntityOperation.queue(context, message, EntityOperation.SEEN, message.ui_seen);
|
||||
}
|
||||
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.sendBroadcast(
|
||||
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
|
||||
.putExtra("folder", message.folder)
|
||||
.putExtra("id", message.id));
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.sendBroadcast(
|
||||
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
|
||||
.putExtra("folder", message.folder)
|
||||
.putExtra("id", message.id));
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -216,10 +223,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
||||
TupleMessageEx message = filtered.get(position);
|
||||
|
||||
if (EntityFolder.isOutgoing(message.folderType)) {
|
||||
holder.tvAddress.setText(message.to == null ? null : MessageHelper.getFormattedAddresses(message.to));
|
||||
holder.tvFrom.setText(MessageHelper.getFormattedAddresses(message.to));
|
||||
holder.tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
|
||||
} else {
|
||||
holder.tvAddress.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
|
||||
holder.tvFrom.setText(MessageHelper.getFormattedAddresses(message.from));
|
||||
holder.tvTime.setText(message.sent == null ? null : DateUtils.getRelativeTimeSpanString(context, message.sent));
|
||||
}
|
||||
|
||||
@@ -232,8 +239,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
||||
|
||||
boolean unseen = (message.thread == null ? !message.seen : message.unseen > 0);
|
||||
int visibility = (unseen ? Typeface.BOLD : Typeface.NORMAL);
|
||||
holder.tvAddress.setTypeface(null, visibility);
|
||||
holder.tvFrom.setTypeface(null, visibility);
|
||||
holder.tvTime.setTypeface(null, visibility);
|
||||
holder.ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
|
||||
holder.tvSubject.setTypeface(null, visibility);
|
||||
holder.tvCount.setTypeface(null, visibility);
|
||||
|
||||
|
||||
@@ -20,6 +20,57 @@ package eu.faircode.email;
|
||||
*/
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.mail.Address;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
public class ApplicationEx extends Application {
|
||||
private Thread.UncaughtExceptionHandler prev = null;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
prev = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable ex) {
|
||||
Log.w(Helper.TAG, "Handling crash");
|
||||
DB db = null;
|
||||
try {
|
||||
db = DB.getBlockingInstance(ApplicationEx.this);
|
||||
EntityFolder drafts = db.folder().getPrimaryDraftFolder();
|
||||
if (drafts != null) {
|
||||
Address to = new InternetAddress("marcel+email@faircode.eu", "FairCode");
|
||||
|
||||
EntityMessage draft = new EntityMessage();
|
||||
draft.account = drafts.account;
|
||||
draft.folder = drafts.id;
|
||||
draft.to = MessageHelper.encodeAddresses(new Address[]{to});
|
||||
draft.subject = BuildConfig.APPLICATION_ID + " crash info";
|
||||
draft.body = "<pre>" + ex.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
|
||||
draft.received = new Date().getTime();
|
||||
draft.seen = false;
|
||||
draft.ui_seen = false;
|
||||
draft.ui_hide = false;
|
||||
draft.id = db.message().insertMessage(draft);
|
||||
|
||||
Log.w(Helper.TAG, "Crash info stored as draft");
|
||||
}
|
||||
} catch (Throwable e1) {
|
||||
Log.e(Helper.TAG, e1 + "\n" + Log.getStackTraceString(e1));
|
||||
} finally {
|
||||
if (db != null)
|
||||
db.close();
|
||||
}
|
||||
|
||||
if (prev != null)
|
||||
prev.uncaughtException(thread, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@ import android.util.Log;
|
||||
EntityAccount.class,
|
||||
EntityFolder.class,
|
||||
EntityMessage.class,
|
||||
EntityAttachment.class,
|
||||
EntityOperation.class
|
||||
},
|
||||
version = 1,
|
||||
version = 2,
|
||||
exportSchema = true
|
||||
)
|
||||
|
||||
@@ -53,6 +54,8 @@ public abstract class DB extends RoomDatabase {
|
||||
|
||||
public abstract DaoMessage message();
|
||||
|
||||
public abstract DaoAttachment attachment();
|
||||
|
||||
public abstract DaoOperation operation();
|
||||
|
||||
private static DB sInstance;
|
||||
@@ -65,9 +68,13 @@ public abstract class DB extends RoomDatabase {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public static DB getBlockingInstance(Context context) {
|
||||
return migrate(Room.databaseBuilder(context.getApplicationContext(), DB.class, DB_NAME).allowMainThreadQueries());
|
||||
}
|
||||
|
||||
private static DB migrate(RoomDatabase.Builder<DB> builder) {
|
||||
return builder
|
||||
//.addMigrations(MIGRATION_1_2)
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -75,7 +82,14 @@ public abstract class DB extends RoomDatabase {
|
||||
@Override
|
||||
public void migrate(SupportSQLiteDatabase db) {
|
||||
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("ALTER TABLE message ADD COLUMN error TEXT");
|
||||
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`)");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
44
app/src/main/java/eu/faircode/email/DaoAttachment.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of Safe email.
|
||||
|
||||
Safe email is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
NetGuard is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface DaoAttachment {
|
||||
@Query("SELECT * FROM attachment WHERE message = :message")
|
||||
LiveData<List<EntityAttachment>> liveAttachments(long message);
|
||||
|
||||
@Query("SELECT * FROM attachment WHERE message = :message AND sequence = :sequence")
|
||||
EntityAttachment getAttachment(long message, int sequence);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
long insertAttachment(EntityAttachment attachment);
|
||||
|
||||
@Update
|
||||
void updateAttachment(EntityAttachment attachment);
|
||||
}
|
||||
@@ -33,26 +33,32 @@ public interface DaoMessage {
|
||||
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
|
||||
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
|
||||
" FROM folder" +
|
||||
" JOIN message ON folder = folder.id" +
|
||||
" WHERE folder.type = '" + EntityFolder.TYPE_INBOX + "'" +
|
||||
" AND NOT ui_hide" +
|
||||
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
|
||||
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder" +
|
||||
" GROUP BY CASE WHEN m.thread IS NULL THEN m.id ELSE m.thread END)")
|
||||
// in theory the message id and thread could be the same
|
||||
LiveData<List<TupleMessageEx>> liveUnifiedInbox();
|
||||
|
||||
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
|
||||
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
|
||||
" FROM folder" +
|
||||
" JOIN message ON folder = folder.id" +
|
||||
" WHERE folder.id = :folder" +
|
||||
" AND NOT ui_hide" +
|
||||
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
|
||||
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder" +
|
||||
" GROUP BY CASE WHEN m.thread IS NULL THEN m.id ELSE m.thread END)")
|
||||
LiveData<List<TupleMessageEx>> liveMessages(long folder);
|
||||
|
||||
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
|
||||
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
|
||||
" FROM message" +
|
||||
" JOIN folder ON folder.id = message.folder" +
|
||||
" JOIN message m1 ON m1.id = :msgid AND m1.account = message.account AND m1.thread = message.thread" +
|
||||
@@ -68,6 +74,7 @@ public interface DaoMessage {
|
||||
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
|
||||
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
|
||||
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
|
||||
" FROM message" +
|
||||
" JOIN folder ON folder.id = message.folder" +
|
||||
" WHERE message.id = :id")
|
||||
|
||||
@@ -21,10 +21,13 @@ package eu.faircode.email;
|
||||
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import javax.mail.BodyPart;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
|
||||
@Entity(
|
||||
@@ -33,7 +36,8 @@ import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
@ForeignKey(childColumns = "message", entity = EntityMessage.class, parentColumns = "id", onDelete = CASCADE)
|
||||
},
|
||||
indices = {
|
||||
@Index(value = {"message"})
|
||||
@Index(value = {"message"}),
|
||||
@Index(value = {"message", "sequence"}, unique = true)
|
||||
}
|
||||
)
|
||||
public class EntityAttachment {
|
||||
@@ -44,7 +48,12 @@ public class EntityAttachment {
|
||||
@NonNull
|
||||
public Long message;
|
||||
@NonNull
|
||||
public String type;
|
||||
public Integer sequence;
|
||||
public String name;
|
||||
@NonNull
|
||||
public String type;
|
||||
public byte[] content;
|
||||
|
||||
@Ignore
|
||||
BodyPart part;
|
||||
}
|
||||
|
||||
@@ -54,25 +54,29 @@ public class EntityFolder {
|
||||
static final String TYPE_SENT = "Sent";
|
||||
static final String TYPE_USER = "User";
|
||||
|
||||
static final List<String> STANDARD_FOLDER_ATTR = Arrays.asList(
|
||||
static final List<String> SYSTEM_FOLDER_ATTR = Arrays.asList(
|
||||
"All",
|
||||
"Drafts",
|
||||
"Trash",
|
||||
"Junk",
|
||||
"Sent"
|
||||
);
|
||||
static final List<String> STANDARD_FOLDER_TYPE = Arrays.asList(
|
||||
static final List<String> SYSTEM_FOLDER_TYPE = Arrays.asList(
|
||||
TYPE_ARCHIVE,
|
||||
TYPE_DRAFTS,
|
||||
TYPE_TRASH,
|
||||
TYPE_JUNK,
|
||||
TYPE_SENT
|
||||
); // Must match STANDARD_FOLDER_ATTR
|
||||
); // Must match SYSTEM_FOLDER_ATTR
|
||||
|
||||
static boolean isOutgoing(String type) {
|
||||
return (TYPE_OUTBOX.equals(type) || TYPE_DRAFTS.equals(type) || TYPE_SENT.equals(type));
|
||||
}
|
||||
|
||||
static Boolean isUser(String type) {
|
||||
return TYPE_USER.equals(type);
|
||||
}
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public Long id;
|
||||
public Long account; // Outbox = null
|
||||
|
||||
@@ -58,6 +58,7 @@ public class EntityOperation {
|
||||
public static final String MOVE = "move";
|
||||
public static final String DELETE = "delete";
|
||||
public static final String SEND = "send";
|
||||
public static final String ATTACHMENT = "attachment";
|
||||
|
||||
static void queue(Context context, EntityMessage message, String name) {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
@@ -74,8 +75,16 @@ public class EntityOperation {
|
||||
DaoOperation dao = DB.getInstance(context).operation();
|
||||
|
||||
int purged = 0;
|
||||
if (SEEN.equals(name))
|
||||
if (SEEN.equals(name)) {
|
||||
if (message.uid == null) {
|
||||
// local message
|
||||
return;
|
||||
}
|
||||
purged = dao.deleteOperations(message.id, name);
|
||||
} else if (DELETE.equals(name)) {
|
||||
if (message.uid == null)
|
||||
return;
|
||||
}
|
||||
|
||||
EntityOperation operation = new EntityOperation();
|
||||
operation.message = message.id;
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
@@ -63,7 +64,7 @@ public class FragmentAccount extends Fragment {
|
||||
private EditText etHost;
|
||||
private EditText etPort;
|
||||
private EditText etUser;
|
||||
private EditText etPassword;
|
||||
private TextInputLayout tilPassword;
|
||||
private CheckBox cbPrimary;
|
||||
private CheckBox cbSynchronize;
|
||||
private Button btnOk;
|
||||
@@ -96,7 +97,7 @@ public class FragmentAccount extends Fragment {
|
||||
etHost = view.findViewById(R.id.etHost);
|
||||
etPort = view.findViewById(R.id.etPort);
|
||||
etUser = view.findViewById(R.id.etUser);
|
||||
etPassword = view.findViewById(R.id.etPassword);
|
||||
tilPassword = view.findViewById(R.id.tilPassword);
|
||||
cbPrimary = view.findViewById(R.id.cbPrimary);
|
||||
cbSynchronize = view.findViewById(R.id.cbSynchronize);
|
||||
btnOk = view.findViewById(R.id.btnOk);
|
||||
@@ -138,7 +139,7 @@ public class FragmentAccount extends Fragment {
|
||||
args.putString("host", etHost.getText().toString());
|
||||
args.putString("port", etPort.getText().toString());
|
||||
args.putString("user", etUser.getText().toString());
|
||||
args.putString("password", etPassword.getText().toString());
|
||||
args.putString("password", tilPassword.getEditText().getText().toString());
|
||||
args.putBoolean("primary", cbPrimary.isChecked());
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
|
||||
@@ -153,7 +154,7 @@ public class FragmentAccount extends Fragment {
|
||||
etHost.setText(account == null ? null : account.host);
|
||||
etPort.setText(account == null ? null : Long.toString(account.port));
|
||||
etUser.setText(account == null ? null : account.user);
|
||||
etPassword.setText(account == null ? null : account.password);
|
||||
tilPassword.getEditText().setText(account == null ? null : account.password);
|
||||
cbPrimary.setChecked(account == null ? true : account.primary);
|
||||
cbSynchronize.setChecked(account == null ? true : account.synchronize);
|
||||
}
|
||||
@@ -217,33 +218,58 @@ public class FragmentAccount extends Fragment {
|
||||
if (!istore.hasCapability("IDLE"))
|
||||
throw new MessagingException(getContext().getString(R.string.title_no_idle));
|
||||
|
||||
// Find system folders
|
||||
boolean drafts = false;
|
||||
for (Folder ifolder : istore.getDefaultFolder().list("*")) {
|
||||
String type = null;
|
||||
|
||||
// First check folder attributes
|
||||
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
|
||||
for (String attr : attrs) {
|
||||
if (attr.startsWith("\\")) {
|
||||
int index = EntityFolder.STANDARD_FOLDER_ATTR.indexOf(attr.substring(1));
|
||||
int index = EntityFolder.SYSTEM_FOLDER_ATTR.indexOf(attr.substring(1));
|
||||
if (index >= 0) {
|
||||
EntityFolder folder = new EntityFolder();
|
||||
folder.name = ifolder.getFullName();
|
||||
folder.type = EntityFolder.STANDARD_FOLDER_TYPE.get(index);
|
||||
folder.synchronize = standard_sync.contains(folder.type);
|
||||
folder.after = DEFAULT_STANDARD_SYNC;
|
||||
folders.add(folder);
|
||||
|
||||
Log.i(Helper.TAG, "Standard folder=" + folder.name +
|
||||
" type=" + folder.type + " attr=" + TextUtils.join(",", attrs));
|
||||
|
||||
if (EntityFolder.TYPE_DRAFTS.equals(folder.type))
|
||||
drafts = true;
|
||||
|
||||
type = EntityFolder.SYSTEM_FOLDER_TYPE.get(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next check folder full name
|
||||
if (type == null) {
|
||||
String fullname = ifolder.getFullName();
|
||||
for (String attr : EntityFolder.SYSTEM_FOLDER_ATTR)
|
||||
if (attr.equals(fullname)) {
|
||||
int index = EntityFolder.SYSTEM_FOLDER_ATTR.indexOf(attr);
|
||||
type = EntityFolder.SYSTEM_FOLDER_TYPE.get(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
EntityFolder folder = new EntityFolder();
|
||||
folder.name = ifolder.getFullName();
|
||||
folder.type = type;
|
||||
folder.synchronize = standard_sync.contains(folder.type);
|
||||
folder.after = DEFAULT_STANDARD_SYNC;
|
||||
folders.add(folder);
|
||||
|
||||
Log.i(Helper.TAG, account.name +
|
||||
" system=" + folder.name +
|
||||
" type=" + folder.type + " attr=" + TextUtils.join(",", attrs));
|
||||
|
||||
if (EntityFolder.TYPE_DRAFTS.equals(folder.type))
|
||||
drafts = true;
|
||||
}
|
||||
}
|
||||
if (!drafts) {
|
||||
EntityFolder folder = new EntityFolder();
|
||||
folder.name = getContext().getString(R.string.title_local_drafts);
|
||||
folder.type = EntityFolder.TYPE_DRAFTS;
|
||||
folder.synchronize = false;
|
||||
folder.after = 0;
|
||||
folders.add(folder);
|
||||
}
|
||||
if (!drafts)
|
||||
throw new MessagingException(getContext().getString(R.string.title_no_drafts));
|
||||
} finally {
|
||||
if (istore != null)
|
||||
istore.close();
|
||||
@@ -253,30 +279,31 @@ public class FragmentAccount extends Fragment {
|
||||
if (account.primary)
|
||||
db.account().resetPrimary();
|
||||
|
||||
if (update)
|
||||
db.account().updateAccount(account);
|
||||
else
|
||||
try {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.beginTransaction();
|
||||
if (update)
|
||||
db.account().updateAccount(account);
|
||||
else
|
||||
account.id = db.account().insertAccount(account);
|
||||
|
||||
EntityFolder inbox = new EntityFolder();
|
||||
inbox.name = "INBOX";
|
||||
inbox.type = EntityFolder.TYPE_INBOX;
|
||||
inbox.synchronize = true;
|
||||
inbox.after = DEFAULT_INBOX_SYNC;
|
||||
folders.add(0, inbox);
|
||||
EntityFolder inbox = new EntityFolder();
|
||||
inbox.name = "INBOX";
|
||||
inbox.type = EntityFolder.TYPE_INBOX;
|
||||
inbox.synchronize = true;
|
||||
inbox.after = DEFAULT_INBOX_SYNC;
|
||||
folders.add(0, inbox);
|
||||
|
||||
for (EntityFolder folder : folders) {
|
||||
for (EntityFolder folder : folders)
|
||||
if (db.folder().getFolder(account.id, folder.name) == null) {
|
||||
folder.account = account.id;
|
||||
Log.i(Helper.TAG, "Creating folder=" + folder.name + " (" + folder.type + ")");
|
||||
folder.id = db.folder().insertFolder(folder);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
ServiceSynchronize.restart(getContext(), "account");
|
||||
|
||||
@@ -292,7 +319,7 @@ public class FragmentAccount extends Fragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
|
||||
PutLoader loader = new PutLoader(getActivity());
|
||||
PutLoader loader = new PutLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -69,13 +71,18 @@ public class FragmentCompose extends Fragment {
|
||||
private long rid = -1;
|
||||
|
||||
private Spinner spFrom;
|
||||
private ImageView ivIdentyAdd;
|
||||
private ImageView ivIdentityAdd;
|
||||
private EditText etTo;
|
||||
private ImageView ivContactAdd;
|
||||
private ImageView ivToAdd;
|
||||
private EditText etCc;
|
||||
private ImageView ivCcAdd;
|
||||
private EditText etBcc;
|
||||
private ImageView ivBccAdd;
|
||||
private EditText etSubject;
|
||||
private EditText etBody;
|
||||
private BottomNavigationView bottom_navigation;
|
||||
private ProgressBar pbWait;
|
||||
private Group grpCc;
|
||||
private Group grpReady;
|
||||
|
||||
@Override
|
||||
@@ -90,45 +97,67 @@ public class FragmentCompose extends Fragment {
|
||||
|
||||
// Get controls
|
||||
spFrom = view.findViewById(R.id.spFrom);
|
||||
ivIdentyAdd = view.findViewById(R.id.ivIdentyAdd);
|
||||
ivIdentityAdd = view.findViewById(R.id.ivIdentityAdd);
|
||||
etTo = view.findViewById(R.id.etTo);
|
||||
ivContactAdd = view.findViewById(R.id.ivContactAdd);
|
||||
ivToAdd = view.findViewById(R.id.ivToAdd);
|
||||
etCc = view.findViewById(R.id.etCc);
|
||||
ivCcAdd = view.findViewById(R.id.ivCcAdd);
|
||||
etBcc = view.findViewById(R.id.etBcc);
|
||||
ivBccAdd = view.findViewById(R.id.ivBccAdd);
|
||||
etSubject = view.findViewById(R.id.etSubject);
|
||||
etBody = view.findViewById(R.id.etBody);
|
||||
bottom_navigation = view.findViewById(R.id.bottom_navigation);
|
||||
pbWait = view.findViewById(R.id.pbWait);
|
||||
grpCc = view.findViewById(R.id.grpCc);
|
||||
grpReady = view.findViewById(R.id.grpReady);
|
||||
|
||||
grpCc.setVisibility(View.GONE);
|
||||
etBody.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
// Wire controls
|
||||
|
||||
ivIdentyAdd.setOnClickListener(new View.OnClickListener() {
|
||||
ivIdentityAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", -1);
|
||||
|
||||
FragmentIdentity fragment = new FragmentIdentity();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
});
|
||||
|
||||
ivContactAdd.setOnClickListener(new View.OnClickListener() {
|
||||
ivToAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT);
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_TO);
|
||||
}
|
||||
});
|
||||
|
||||
ivCcAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_CC);
|
||||
}
|
||||
});
|
||||
|
||||
ivBccAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_BCC);
|
||||
}
|
||||
});
|
||||
|
||||
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
bottom_navigation.setEnabled(false);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_delete:
|
||||
actionDelete(id);
|
||||
@@ -145,11 +174,13 @@ public class FragmentCompose extends Fragment {
|
||||
}
|
||||
});
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// Initialize
|
||||
grpReady.setVisibility(View.GONE);
|
||||
pbWait.setVisibility(View.VISIBLE);
|
||||
bottom_navigation.getMenu().findItem(R.id.action_delete).setEnabled(id > 0);
|
||||
bottom_navigation.setEnabled(false);
|
||||
bottom_navigation.getMenu().findItem(R.id.action_delete).setVisible(id > 0);
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, false);
|
||||
|
||||
DB.getInstance(getContext()).identity().liveIdentities(true).observe(getActivity(), new Observer<List<EntityIdentity>>() {
|
||||
@Override
|
||||
@@ -185,9 +216,30 @@ public class FragmentCompose extends Fragment {
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_compose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_cc, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_cc:
|
||||
onMenuCc();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMenuCc() {
|
||||
grpCc.setVisibility(grpCc.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == ActivityCompose.REQUEST_CONTACT && resultCode == RESULT_OK) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = getContext().getContentResolver().query(data.getData(),
|
||||
@@ -202,12 +254,26 @@ public class FragmentCompose extends Fragment {
|
||||
String email = cursor.getString(colEmail);
|
||||
String name = cursor.getString(colName);
|
||||
|
||||
String text = null;
|
||||
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
|
||||
text = etTo.getText().toString();
|
||||
else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
|
||||
text = etCc.getText().toString();
|
||||
else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
|
||||
text = etBcc.getText().toString();
|
||||
|
||||
InternetAddress address = new InternetAddress(email, name);
|
||||
StringBuilder sb = new StringBuilder(etTo.getText().toString());
|
||||
StringBuilder sb = new StringBuilder(text);
|
||||
if (sb.length() > 0)
|
||||
sb.append("; ");
|
||||
sb.append(address.toString());
|
||||
etTo.setText(sb.toString());
|
||||
|
||||
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
|
||||
etTo.setText(sb.toString());
|
||||
else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
|
||||
etCc.setText(sb.toString());
|
||||
else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
|
||||
etBcc.setText(sb.toString());
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
@@ -220,13 +286,15 @@ public class FragmentCompose extends Fragment {
|
||||
}
|
||||
|
||||
private void actionDelete(final long id) {
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, false);
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", id);
|
||||
getLoaderManager().restartLoader(ActivityCompose.LOADER_COMPOSE_DELETE, args, deleteLoaderCallbacks).forceLoad();
|
||||
}
|
||||
|
||||
private void actionPut(long id, boolean send) {
|
||||
bottom_navigation.setEnabled(false);
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, false);
|
||||
|
||||
EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem();
|
||||
|
||||
@@ -236,6 +304,8 @@ public class FragmentCompose extends Fragment {
|
||||
args.putString("thread", FragmentCompose.this.thread);
|
||||
args.putLong("rid", FragmentCompose.this.rid);
|
||||
args.putString("to", etTo.getText().toString());
|
||||
args.putString("cc", etCc.getText().toString());
|
||||
args.putString("bcc", etBcc.getText().toString());
|
||||
args.putString("subject", etSubject.getText().toString());
|
||||
args.putString("body", etBody.getText().toString());
|
||||
args.putBoolean("send", send);
|
||||
@@ -257,63 +327,68 @@ public class FragmentCompose extends Fragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle loadInBackground() {
|
||||
String action = args.getString("action");
|
||||
long id = args.getLong("id", -1);
|
||||
EntityMessage msg = DB.getInstance(getContext()).message().getMessage(id);
|
||||
|
||||
Bundle result = new Bundle();
|
||||
result.putString("action", action);
|
||||
try {
|
||||
String action = args.getString("action");
|
||||
long id = args.getLong("id", -1);
|
||||
EntityMessage msg = DB.getInstance(getContext()).message().getMessage(id);
|
||||
|
||||
if (msg != null) {
|
||||
if (msg.identity != null)
|
||||
result.putLong("iid", msg.identity);
|
||||
if (msg.replying != null)
|
||||
result.putLong("rid", msg.replying);
|
||||
result.putString("thread", msg.thread);
|
||||
result.putString("subject", msg.subject);
|
||||
result.putString("body", msg.body);
|
||||
}
|
||||
result.putString("action", action);
|
||||
|
||||
if (TextUtils.isEmpty(action)) {
|
||||
if (msg != null) {
|
||||
result.putString("from", msg.from);
|
||||
result.putString("to", msg.to);
|
||||
if (msg.identity != null)
|
||||
result.putLong("iid", msg.identity);
|
||||
if (msg.replying != null)
|
||||
result.putLong("rid", msg.replying);
|
||||
result.putString("cc", msg.cc);
|
||||
result.putString("bcc", msg.bcc);
|
||||
result.putString("thread", msg.thread);
|
||||
result.putString("subject", msg.subject);
|
||||
result.putString("body", msg.body);
|
||||
}
|
||||
} else if ("reply".equals(action)) {
|
||||
String to = null;
|
||||
if (msg != null)
|
||||
try {
|
||||
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
|
||||
to = (reply.length == 0 ? msg.from : msg.reply);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
result.putLong("rid", msg.id);
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", to);
|
||||
} else if ("reply_all".equals(action)) {
|
||||
String to = null;
|
||||
if (msg != null) {
|
||||
try {
|
||||
Address[] from = MessageHelper.decodeAddresses(msg.from);
|
||||
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
|
||||
Address[] cc = MessageHelper.decodeAddresses(msg.cc);
|
||||
List<Address> addresses = new ArrayList<>();
|
||||
addresses.addAll(Arrays.asList(reply.length == 0 ? from : reply));
|
||||
addresses.addAll(Arrays.asList(cc));
|
||||
to = MessageHelper.encodeAddresses(addresses.toArray(new Address[0]));
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
result.putLong("rid", msg.id);
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", to);
|
||||
} else if ("forward".equals(action)) {
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(action)) {
|
||||
if (msg != null) {
|
||||
result.putString("from", msg.from);
|
||||
result.putString("to", msg.to);
|
||||
}
|
||||
} else if ("reply".equals(action)) {
|
||||
String to = null;
|
||||
if (msg != null)
|
||||
try {
|
||||
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
|
||||
to = (reply.length == 0 ? msg.from : msg.reply);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
result.putLong("rid", msg.id);
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", to);
|
||||
} else if ("reply_all".equals(action)) {
|
||||
String to = null;
|
||||
if (msg != null) {
|
||||
try {
|
||||
Address[] from = MessageHelper.decodeAddresses(msg.from);
|
||||
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
|
||||
Address[] cc = MessageHelper.decodeAddresses(msg.cc);
|
||||
List<Address> addresses = new ArrayList<>();
|
||||
addresses.addAll(Arrays.asList(reply.length == 0 ? from : reply));
|
||||
addresses.addAll(Arrays.asList(cc));
|
||||
to = MessageHelper.encodeAddresses(addresses.toArray(new Address[0]));
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
result.putLong("rid", msg.id);
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", to);
|
||||
} else if ("forward".equals(action)) {
|
||||
result.putString("from", msg.to);
|
||||
result.putString("to", null);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -322,13 +397,13 @@ public class FragmentCompose extends Fragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Bundle> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
GetLoader loader = new GetLoader(getActivity());
|
||||
GetLoader loader = new GetLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Bundle> loader, final Bundle result) {
|
||||
public void onLoadFinished(@NonNull Loader<Bundle> loader, Bundle result) {
|
||||
getLoaderManager().destroyLoader(loader.getId());
|
||||
|
||||
long iid = result.getLong("iid", -1);
|
||||
@@ -336,6 +411,8 @@ public class FragmentCompose extends Fragment {
|
||||
String thread = result.getString("thread");
|
||||
String from = result.getString("from");
|
||||
String to = result.getString("to");
|
||||
String cc = result.getString("cc");
|
||||
String bcc = result.getString("bcc");
|
||||
String subject = result.getString("subject");
|
||||
String body = result.getString("body");
|
||||
String action = result.getString("action");
|
||||
@@ -360,9 +437,11 @@ public class FragmentCompose extends Fragment {
|
||||
// Prevent changed fields from being overwritten
|
||||
once = true;
|
||||
|
||||
etCc.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(cc)));
|
||||
etBcc.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(bcc)));
|
||||
|
||||
if (action == null) {
|
||||
if (to != null)
|
||||
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
|
||||
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
|
||||
etSubject.setText(subject);
|
||||
if (body != null)
|
||||
etBody.setText(Html.fromHtml(HtmlHelper.sanitize(getContext(), body, false)));
|
||||
@@ -384,7 +463,7 @@ public class FragmentCompose extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
bottom_navigation.setEnabled(true);
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -405,15 +484,19 @@ public class FragmentCompose extends Fragment {
|
||||
|
||||
@Override
|
||||
public Throwable loadInBackground() {
|
||||
long id = args.getLong("id");
|
||||
DaoMessage message = DB.getInstance(getContext()).message();
|
||||
EntityMessage draft = message.getMessage(id);
|
||||
if (draft != null) {
|
||||
draft.ui_hide = true;
|
||||
message.updateMessage(draft);
|
||||
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
|
||||
try {
|
||||
long id = args.getLong("id");
|
||||
DaoMessage message = DB.getInstance(getContext()).message();
|
||||
EntityMessage draft = message.getMessage(id);
|
||||
if (draft != null) {
|
||||
draft.ui_hide = true;
|
||||
message.updateMessage(draft);
|
||||
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
|
||||
}
|
||||
return null;
|
||||
} catch (Throwable ex) {
|
||||
return ex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +504,7 @@ public class FragmentCompose extends Fragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Throwable> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
DeleteLoader loader = new DeleteLoader(getActivity());
|
||||
DeleteLoader loader = new DeleteLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
@@ -433,6 +516,10 @@ public class FragmentCompose extends Fragment {
|
||||
if (ex == null) {
|
||||
getFragmentManager().popBackStack();
|
||||
Toast.makeText(getContext(), R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, true);
|
||||
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,11 +560,15 @@ public class FragmentCompose extends Fragment {
|
||||
long rid = args.getLong("rid", -1);
|
||||
String thread = args.getString("thread");
|
||||
String to = args.getString("to");
|
||||
String cc = args.getString("cc");
|
||||
String bcc = args.getString("bcc");
|
||||
String body = args.getString("body");
|
||||
String subject = args.getString("subject");
|
||||
|
||||
Address afrom = (ident == null ? null : new InternetAddress(ident.email, ident.name));
|
||||
Address ato[] = (TextUtils.isEmpty(to) ? new Address[0] : InternetAddress.parse(to));
|
||||
Address ato[] = (TextUtils.isEmpty(to) ? null : InternetAddress.parse(to));
|
||||
Address acc[] = (TextUtils.isEmpty(cc) ? null : InternetAddress.parse(cc));
|
||||
Address abcc[] = (TextUtils.isEmpty(bcc) ? null : InternetAddress.parse(bcc));
|
||||
|
||||
// Build draft
|
||||
boolean update = (draft != null);
|
||||
@@ -488,8 +579,10 @@ public class FragmentCompose extends Fragment {
|
||||
draft.identity = (ident == null ? null : ident.id);
|
||||
draft.replying = (rid < 0 ? null : rid);
|
||||
draft.thread = thread;
|
||||
draft.from = (afrom == null ? null : MessageHelper.encodeAddresses(new Address[]{afrom}));
|
||||
draft.to = (ato == null ? null : MessageHelper.encodeAddresses(ato));
|
||||
draft.from = MessageHelper.encodeAddresses(new Address[]{afrom});
|
||||
draft.to = MessageHelper.encodeAddresses(ato);
|
||||
draft.cc = MessageHelper.encodeAddresses(acc);
|
||||
draft.bcc = MessageHelper.encodeAddresses(abcc);
|
||||
draft.subject = subject;
|
||||
draft.body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
|
||||
draft.received = new Date().getTime();
|
||||
@@ -507,28 +600,19 @@ public class FragmentCompose extends Fragment {
|
||||
if (send) {
|
||||
if (draft.identity == null)
|
||||
throw new MessagingException(getContext().getString(R.string.title_from_missing));
|
||||
if (draft.to == null)
|
||||
if (draft.to == null && draft.cc == null && draft.bcc == null)
|
||||
throw new MessagingException(getContext().getString(R.string.title_to_missing));
|
||||
|
||||
// Get outbox
|
||||
EntityFolder outbox = folder.getOutbox();
|
||||
if (outbox == null) {
|
||||
outbox = new EntityFolder();
|
||||
outbox.name = "OUTBOX";
|
||||
outbox.type = EntityFolder.TYPE_OUTBOX;
|
||||
outbox.synchronize = false;
|
||||
outbox.after = 0;
|
||||
outbox.id = folder.insertFolder(outbox);
|
||||
}
|
||||
|
||||
// Build outgoing message
|
||||
EntityMessage out = new EntityMessage();
|
||||
out.folder = outbox.id;
|
||||
out.folder = folder.getOutbox().id;
|
||||
out.identity = draft.identity;
|
||||
out.replying = draft.replying;
|
||||
out.thread = draft.thread;
|
||||
out.from = draft.from;
|
||||
out.to = draft.to;
|
||||
out.cc = draft.cc;
|
||||
out.bcc = draft.bcc;
|
||||
out.subject = draft.subject;
|
||||
out.body = draft.body;
|
||||
out.received = draft.received;
|
||||
@@ -557,7 +641,7 @@ public class FragmentCompose extends Fragment {
|
||||
@Override
|
||||
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
|
||||
this.args = args;
|
||||
PutLoader loader = new PutLoader(getActivity());
|
||||
PutLoader loader = new PutLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
@@ -574,6 +658,7 @@ public class FragmentCompose extends Fragment {
|
||||
Toast.makeText(getContext(), send ? R.string.title_queued : R.string.title_draft_saved, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
bottom_navigation.getMenu().setGroupEnabled(0, true);
|
||||
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public class FragmentFolder extends Fragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
|
||||
PutLoader loader = new PutLoader(getActivity());
|
||||
PutLoader loader = new PutLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -86,6 +87,9 @@ public class FragmentFolders extends Fragment {
|
||||
}
|
||||
});
|
||||
|
||||
// Show hint
|
||||
Toast.makeText(getContext(), R.string.title_item_edit_hint, Toast.LENGTH_SHORT).show();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
@@ -59,11 +60,12 @@ public class FragmentIdentity extends Fragment {
|
||||
private Spinner spProfile;
|
||||
private EditText etName;
|
||||
private EditText etEmail;
|
||||
private EditText etReplyTo;
|
||||
private EditText etHost;
|
||||
private CheckBox cbStartTls;
|
||||
private EditText etPort;
|
||||
private EditText etUser;
|
||||
private EditText etPassword;
|
||||
private TextInputLayout tilPassword;
|
||||
private CheckBox cbPrimary;
|
||||
private CheckBox cbSynchronize;
|
||||
private Button btnOk;
|
||||
@@ -86,11 +88,12 @@ public class FragmentIdentity extends Fragment {
|
||||
spProfile = view.findViewById(R.id.spProvider);
|
||||
etName = view.findViewById(R.id.etName);
|
||||
etEmail = view.findViewById(R.id.etEmail);
|
||||
etReplyTo = view.findViewById(R.id.etReplyTo);
|
||||
etHost = view.findViewById(R.id.etHost);
|
||||
cbStartTls = view.findViewById(R.id.cbStartTls);
|
||||
etPort = view.findViewById(R.id.etPort);
|
||||
etUser = view.findViewById(R.id.etUser);
|
||||
etPassword = view.findViewById(R.id.etPassword);
|
||||
tilPassword = view.findViewById(R.id.tilPassword);
|
||||
cbPrimary = view.findViewById(R.id.cbPrimary);
|
||||
cbSynchronize = view.findViewById(R.id.cbSynchronize);
|
||||
btnOk = view.findViewById(R.id.btnOk);
|
||||
@@ -152,11 +155,12 @@ public class FragmentIdentity extends Fragment {
|
||||
args.putLong("id", id);
|
||||
args.putString("name", etName.getText().toString());
|
||||
args.putString("email", etEmail.getText().toString());
|
||||
args.putString("replyto", etReplyTo.getText().toString());
|
||||
args.putString("host", etHost.getText().toString());
|
||||
args.putBoolean("starttls", cbStartTls.isChecked());
|
||||
args.putString("port", etPort.getText().toString());
|
||||
args.putString("user", etUser.getText().toString());
|
||||
args.putString("password", etPassword.getText().toString());
|
||||
args.putString("password", tilPassword.getEditText().getText().toString());
|
||||
args.putBoolean("primary", cbPrimary.isChecked());
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
|
||||
@@ -169,11 +173,12 @@ public class FragmentIdentity extends Fragment {
|
||||
public void onChanged(@Nullable EntityIdentity identity) {
|
||||
etName.setText(identity == null ? null : identity.name);
|
||||
etEmail.setText(identity == null ? null : identity.email);
|
||||
etReplyTo.setText(identity == null ? null : identity.replyto);
|
||||
etHost.setText(identity == null ? null : identity.host);
|
||||
cbStartTls.setChecked(identity == null ? false : identity.starttls);
|
||||
etPort.setText(identity == null ? null : Long.toString(identity.port));
|
||||
etUser.setText(identity == null ? null : identity.user);
|
||||
etPassword.setText(identity == null ? null : identity.password);
|
||||
tilPassword.getEditText().setText(identity == null ? null : identity.password);
|
||||
cbPrimary.setChecked(identity == null ? true : identity.primary);
|
||||
cbSynchronize.setChecked(identity == null ? true : identity.synchronize);
|
||||
}
|
||||
@@ -185,7 +190,7 @@ public class FragmentIdentity extends Fragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_indentity);
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_identity);
|
||||
}
|
||||
|
||||
private static class PutLoader extends AsyncTaskLoader<Throwable> {
|
||||
@@ -203,9 +208,13 @@ public class FragmentIdentity extends Fragment {
|
||||
public Throwable loadInBackground() {
|
||||
try {
|
||||
long id = args.getLong("id");
|
||||
String replyto = args.getString("replyto");
|
||||
String host = args.getString("host");
|
||||
boolean starttls = args.getBoolean("starttls");
|
||||
String port = args.getString("port");
|
||||
|
||||
if (TextUtils.isEmpty(replyto))
|
||||
replyto = null;
|
||||
if (TextUtils.isEmpty(port))
|
||||
port = "0";
|
||||
|
||||
@@ -216,6 +225,7 @@ public class FragmentIdentity extends Fragment {
|
||||
identity = new EntityIdentity();
|
||||
identity.name = Objects.requireNonNull(args.getString("name"));
|
||||
identity.email = Objects.requireNonNull(args.getString("email"));
|
||||
identity.replyto = replyto;
|
||||
identity.host = host;
|
||||
identity.port = Integer.parseInt(port);
|
||||
identity.starttls = starttls;
|
||||
@@ -262,7 +272,7 @@ public class FragmentIdentity extends Fragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
|
||||
PutLoader loader = new PutLoader(getActivity());
|
||||
PutLoader loader = new PutLoader(getContext());
|
||||
loader.setArgs(args);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,19 @@ import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Html;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
@@ -46,20 +55,28 @@ import android.widget.Toast;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class FragmentMessage extends Fragment {
|
||||
private TextView tvTime;
|
||||
private TextView tvFrom;
|
||||
private TextView tvTo;
|
||||
private TextView tvCc;
|
||||
private TextView tvBcc;
|
||||
private RecyclerView rvAttachment;
|
||||
private TextView tvSubject;
|
||||
private TextView tvCount;
|
||||
private BottomNavigationView top_navigation;
|
||||
private TextView tvBody;
|
||||
private BottomNavigationView bottom_navigation;
|
||||
private ProgressBar pbWait;
|
||||
private Group grpAddress;
|
||||
private Group grpAttachments;
|
||||
private Group grpReady;
|
||||
|
||||
private AdapterAttachment adapter;
|
||||
private LiveData<TupleFolderEx> liveFolder;
|
||||
|
||||
private ExecutorService executor = Executors.newCachedThreadPool();
|
||||
@@ -76,7 +93,11 @@ public class FragmentMessage extends Fragment {
|
||||
final long id = args.getLong("id");
|
||||
|
||||
// Get controls
|
||||
tvFrom = view.findViewById(R.id.tvAddress);
|
||||
tvFrom = view.findViewById(R.id.tvFrom);
|
||||
tvTo = view.findViewById(R.id.tvTo);
|
||||
tvCc = view.findViewById(R.id.tvCc);
|
||||
tvBcc = view.findViewById(R.id.tvBcc);
|
||||
rvAttachment = view.findViewById(R.id.rvAttachment);
|
||||
tvTime = view.findViewById(R.id.tvTime);
|
||||
tvSubject = view.findViewById(R.id.tvSubject);
|
||||
tvCount = view.findViewById(R.id.tvCount);
|
||||
@@ -84,13 +105,45 @@ public class FragmentMessage extends Fragment {
|
||||
tvBody = view.findViewById(R.id.tvBody);
|
||||
bottom_navigation = view.findViewById(R.id.bottom_navigation);
|
||||
pbWait = view.findViewById(R.id.pbWait);
|
||||
grpAddress = view.findViewById(R.id.grpAddress);
|
||||
grpAttachments = view.findViewById(R.id.grpAttachments);
|
||||
grpReady = view.findViewById(R.id.grpReady);
|
||||
|
||||
tvTime.setTextIsSelectable(true);
|
||||
tvFrom.setTextIsSelectable(true);
|
||||
tvSubject.setTextIsSelectable(true);
|
||||
tvBody.setTextIsSelectable(true);
|
||||
tvBody.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
tvBody.setMovementMethod(new LinkMovementMethod() {
|
||||
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
|
||||
if (event.getAction() != MotionEvent.ACTION_UP)
|
||||
return super.onTouchEvent(widget, buffer, event);
|
||||
|
||||
int x = (int) event.getX();
|
||||
int y = (int) event.getY();
|
||||
|
||||
x -= widget.getTotalPaddingLeft();
|
||||
y -= widget.getTotalPaddingTop();
|
||||
|
||||
x += widget.getScrollX();
|
||||
y += widget.getScrollY();
|
||||
|
||||
Layout layout = widget.getLayout();
|
||||
int line = layout.getLineForVertical(y);
|
||||
int off = layout.getOffsetForHorizontal(line, x);
|
||||
|
||||
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
|
||||
if (link.length != 0) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("link", link[0].getURL());
|
||||
|
||||
FragmentWebView fragment = new FragmentWebView();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Wire controls
|
||||
|
||||
@@ -140,10 +193,19 @@ public class FragmentMessage extends Fragment {
|
||||
});
|
||||
|
||||
// Initialize
|
||||
grpAddress.setVisibility(View.GONE);
|
||||
grpAttachments.setVisibility(View.GONE);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
pbWait.setVisibility(View.VISIBLE);
|
||||
|
||||
DB db = DB.getInstance(getContext());
|
||||
rvAttachment.setHasFixedSize(false);
|
||||
LinearLayoutManager llm = new LinearLayoutManager(getContext());
|
||||
rvAttachment.setLayoutManager(llm);
|
||||
|
||||
adapter = new AdapterAttachment(getContext());
|
||||
rvAttachment.setAdapter(adapter);
|
||||
|
||||
final DB db = DB.getInstance(getContext());
|
||||
|
||||
// Observe folder
|
||||
liveFolder = db.folder().liveFolderEx(folder);
|
||||
@@ -160,11 +222,13 @@ public class FragmentMessage extends Fragment {
|
||||
if (FragmentMessage.this.isVisible())
|
||||
getFragmentManager().popBackStack();
|
||||
} else {
|
||||
tvFrom.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
|
||||
tvFrom.setText(MessageHelper.getFormattedAddresses(message.from));
|
||||
tvTo.setText(MessageHelper.getFormattedAddresses(message.to));
|
||||
tvCc.setText(MessageHelper.getFormattedAddresses(message.cc));
|
||||
tvBcc.setText(MessageHelper.getFormattedAddresses(message.bcc));
|
||||
tvTime.setText(message.sent == null ? null : df.format(new Date(message.sent)));
|
||||
tvSubject.setText(message.subject);
|
||||
tvCount.setText(Integer.toString(message.count));
|
||||
tvCount.setVisibility(message.count > 1 ? View.VISIBLE : View.GONE);
|
||||
|
||||
int visibility = (message.ui_seen ? Typeface.NORMAL : Typeface.BOLD);
|
||||
tvFrom.setTypeface(null, visibility);
|
||||
@@ -172,12 +236,20 @@ public class FragmentMessage extends Fragment {
|
||||
tvSubject.setTypeface(null, visibility);
|
||||
tvCount.setTypeface(null, visibility);
|
||||
|
||||
// Observe attachments
|
||||
db.attachment().liveAttachments(id).removeObservers(FragmentMessage.this);
|
||||
db.attachment().liveAttachments(id).observe(FragmentMessage.this, attachmentsObserver);
|
||||
|
||||
top_navigation.getMenu().findItem(R.id.action_thread).setVisible(message.count > 1);
|
||||
|
||||
MenuItem actionSeen = top_navigation.getMenu().findItem(R.id.action_seen);
|
||||
actionSeen.setIcon(message.ui_seen
|
||||
? R.drawable.baseline_visibility_off_24
|
||||
: R.drawable.baseline_visibility_24);
|
||||
actionSeen.setTitle(message.ui_seen ? R.string.title_unseen : R.string.title_seen);
|
||||
|
||||
bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.account != null);
|
||||
bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.account != null);
|
||||
tvBody.setText(message.body == null
|
||||
? null
|
||||
: Html.fromHtml(HtmlHelper.sanitize(getContext(), message.body, false)));
|
||||
@@ -200,6 +272,28 @@ public class FragmentMessage extends Fragment {
|
||||
liveFolder.removeObservers(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_cc, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_cc:
|
||||
onMenuCc();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMenuCc() {
|
||||
if (grpReady.getVisibility() == View.VISIBLE)
|
||||
grpAddress.setVisibility(grpAddress.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
Observer<TupleFolderEx> folderObserver = new Observer<TupleFolderEx>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable TupleFolderEx folder) {
|
||||
@@ -209,23 +303,36 @@ public class FragmentMessage extends Fragment {
|
||||
}
|
||||
};
|
||||
|
||||
Observer<List<EntityAttachment>> attachmentsObserver = new Observer<List<EntityAttachment>>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable List<EntityAttachment> attachments) {
|
||||
adapter.set(attachments);
|
||||
grpAttachments.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
};
|
||||
|
||||
private void onActionSeen(final long id) {
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
message.ui_seen = !message.ui_seen;
|
||||
db.message().updateMessage(message);
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.SEEN, message.ui_seen);
|
||||
try {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
message.ui_seen = !message.ui_seen;
|
||||
db.message().updateMessage(message);
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.SEEN, message.ui_seen);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onActionThread(long id) {
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("thread", id); // message ID
|
||||
|
||||
FragmentMessages fragment = new FragmentMessages();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
@@ -259,12 +366,16 @@ public class FragmentMessage extends Fragment {
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
try {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.DELETE);
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.DELETE);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -282,18 +393,22 @@ public class FragmentMessage extends Fragment {
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
EntityFolder spam = db.folder().getSpamFolder(message.account);
|
||||
if (spam == null) {
|
||||
Toast.makeText(getContext(), R.string.title_no_spam, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
try {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
EntityFolder spam = db.folder().getSpamFolder(message.account);
|
||||
if (spam == null) {
|
||||
Toast.makeText(getContext(), R.string.title_no_spam, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, spam.id);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, spam.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -305,18 +420,22 @@ public class FragmentMessage extends Fragment {
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
EntityFolder archive = db.folder().getArchiveFolder(message.account);
|
||||
if (archive == null) {
|
||||
Toast.makeText(getContext(), R.string.title_no_archive, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
try {
|
||||
DB db = DB.getInstance(getContext());
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
EntityFolder archive = db.folder().getArchiveFolder(message.account);
|
||||
if (archive == null) {
|
||||
Toast.makeText(getContext(), R.string.title_no_archive, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, archive.id);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
message.ui_hide = true;
|
||||
db.message().updateMessage(message);
|
||||
|
||||
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, archive.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package eu.faircode.email;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.Observer;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -28,6 +29,9 @@ import android.support.annotation.Nullable;
|
||||
import android.support.constraint.Group;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -88,6 +92,7 @@ public class FragmentMessages extends Fragment {
|
||||
tvNoEmail.setVisibility(View.GONE);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
pbWait.setVisibility(View.VISIBLE);
|
||||
fab.setVisibility(View.GONE);
|
||||
|
||||
DB db = DB.getInstance(getContext());
|
||||
|
||||
@@ -114,6 +119,8 @@ public class FragmentMessages extends Fragment {
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_folder_thread);
|
||||
else
|
||||
liveFolder.observe(this, folderObserver);
|
||||
|
||||
getLoaderManager().restartLoader(ActivityView.LOADER_MESSAGES_INIT, new Bundle(), initLoaderCallbacks).forceLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,4 +156,36 @@ public class FragmentMessages extends Fragment {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static class InitLoader extends AsyncTaskLoader<Bundle> {
|
||||
public InitLoader(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle loadInBackground() {
|
||||
Bundle result = new Bundle();
|
||||
EntityFolder drafts = DB.getInstance(getContext()).folder().getPrimaryDraftFolder();
|
||||
result.putBoolean("drafts", drafts != null);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private LoaderManager.LoaderCallbacks initLoaderCallbacks = new LoaderManager.LoaderCallbacks<Bundle>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Bundle> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
return new InitLoader(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Bundle> loader, Bundle data) {
|
||||
fab.setVisibility(data.getBoolean("drafts", false) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Bundle> loader) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class FragmentSetup extends Fragment {
|
||||
private Button btnAccount;
|
||||
@@ -48,6 +50,8 @@ public class FragmentSetup extends Fragment {
|
||||
private TextView tvIdentityDone;
|
||||
private TextView tvPermissionsDone;
|
||||
|
||||
private ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private static final String[] permissions = new String[]{
|
||||
Manifest.permission.READ_CONTACTS
|
||||
};
|
||||
@@ -86,11 +90,14 @@ public class FragmentSetup extends Fragment {
|
||||
|
||||
if (!once) {
|
||||
once = true;
|
||||
|
||||
Bundle args = new Bundle();
|
||||
if (account != null)
|
||||
args.putLong("id", account.id);
|
||||
|
||||
FragmentAccount fragment = new FragmentAccount();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
|
||||
fragmentTransaction.commit();
|
||||
@@ -120,8 +127,10 @@ public class FragmentSetup extends Fragment {
|
||||
Bundle args = new Bundle();
|
||||
if (identity != null)
|
||||
args.putLong("id", identity.id);
|
||||
|
||||
FragmentIdentity fragment = new FragmentIdentity();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
|
||||
fragmentTransaction.commit();
|
||||
@@ -146,7 +155,7 @@ public class FragmentSetup extends Fragment {
|
||||
tvIdentityDone.setVisibility(View.INVISIBLE);
|
||||
tvPermissionsDone.setVisibility(View.INVISIBLE);
|
||||
|
||||
DB db = DB.getInstance(getContext());
|
||||
final DB db = DB.getInstance(getContext());
|
||||
|
||||
db.account().liveAccounts(true).observe(this, new Observer<List<EntityAccount>>() {
|
||||
@Override
|
||||
@@ -168,6 +177,22 @@ public class FragmentSetup extends Fragment {
|
||||
|
||||
onRequestPermissionsResult(0, permissions, grantResults);
|
||||
|
||||
// Creat outbox
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
EntityFolder outbox = db.folder().getOutbox();
|
||||
if (outbox == null) {
|
||||
outbox = new EntityFolder();
|
||||
outbox.name = "OUTBOX";
|
||||
outbox.type = EntityFolder.TYPE_OUTBOX;
|
||||
outbox.synchronize = false;
|
||||
outbox.after = 0;
|
||||
outbox.id = db.folder().insertFolder(outbox);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
84
app/src/main/java/eu/faircode/email/FragmentWebView.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of Safe email.
|
||||
|
||||
Safe email is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
NetGuard is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
// https://developer.android.com/reference/android/webkit/WebView
|
||||
|
||||
public class FragmentWebView extends Fragment {
|
||||
private String url = null;
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_webview, container, false);
|
||||
|
||||
final ProgressBar progressBar = view.findViewById(R.id.progressbar);
|
||||
WebView webview = view.findViewById(R.id.webview);
|
||||
|
||||
progressBar.setProgress(0);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
|
||||
WebSettings settings = webview.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setLoadWithOverviewMode(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
//settings.setBuiltInZoomControls(true);
|
||||
|
||||
webview.setWebViewClient(new WebViewClient() {
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
webview.setWebChromeClient(new WebChromeClient() {
|
||||
public void onProgressChanged(WebView view, int progress) {
|
||||
progressBar.setProgress(progress);
|
||||
if (progress == 100)
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
url = getArguments().getString("link");
|
||||
webview.loadUrl(url);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(url);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,14 @@ public class Helper {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static String humanReadableByteCount(long bytes, boolean si) {
|
||||
int unit = si ? 1000 : 1024;
|
||||
if (bytes < unit) return bytes + " B";
|
||||
int exp = (int) (Math.log(bytes) / Math.log(unit));
|
||||
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
|
||||
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
|
||||
}
|
||||
|
||||
static StringBuilder getDebugInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package eu.faircode.email;
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
@@ -36,7 +37,7 @@ import java.util.List;
|
||||
public class HtmlHelper implements NodeVisitor {
|
||||
private Context context;
|
||||
private String newline;
|
||||
private List<String> links = new ArrayList<>();
|
||||
private List<String> refs = new ArrayList<>();
|
||||
private StringBuilder sb = new StringBuilder();
|
||||
|
||||
private HtmlHelper(Context context, boolean reply) {
|
||||
@@ -61,28 +62,36 @@ public class HtmlHelper implements NodeVisitor {
|
||||
if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5", "div"))
|
||||
sb.append(newline);
|
||||
else if (name.equals("a")) {
|
||||
String link = node.absUrl("href");
|
||||
if (!TextUtils.isEmpty(link)) {
|
||||
if (!links.contains(link))
|
||||
links.add(link);
|
||||
sb.append(" ").append(context.getString(R.string.title_link, link, links.size()));
|
||||
String ref = node.absUrl("href");
|
||||
if (!TextUtils.isEmpty(ref)) {
|
||||
if (!refs.contains(ref))
|
||||
refs.add(ref);
|
||||
String alt = node.attr("alt");
|
||||
if (TextUtils.isEmpty(alt))
|
||||
alt = context.getString(R.string.title_link);
|
||||
alt = Html.escapeHtml(alt);
|
||||
sb.append(" ").append(String.format("<a href=\"%s\">%s [%d]</a>", ref, alt, refs.size()));
|
||||
}
|
||||
} else if (name.equals("img")) {
|
||||
String link = node.absUrl("src");
|
||||
if (!TextUtils.isEmpty(link)) {
|
||||
if (!links.contains(link))
|
||||
links.add(link);
|
||||
sb.append(" ").append(context.getString(R.string.title_image, link, links.size()));
|
||||
String ref = node.absUrl("src");
|
||||
if (!TextUtils.isEmpty(ref)) {
|
||||
if (!refs.contains(ref))
|
||||
refs.add(ref);
|
||||
String alt = node.attr("alt");
|
||||
if (TextUtils.isEmpty(alt))
|
||||
alt = context.getString(R.string.title_image);
|
||||
alt = Html.escapeHtml(alt);
|
||||
sb.append(" ").append(String.format("<a href=\"%s\">%s [%d]</a>", ref, alt, refs.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (links.size() > 0)
|
||||
if (refs.size() > 0)
|
||||
sb.append(newline).append(newline);
|
||||
for (int i = 0; i < links.size(); i++)
|
||||
sb.append(String.format("[%d] %s ", i + 1, links.get(i))).append(newline);
|
||||
for (int i = 0; i < refs.size(); i++)
|
||||
sb.append(String.format("[%d] %s ", i + 1, refs.get(i))).append(newline);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,14 @@ import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.mail.Address;
|
||||
import javax.mail.BodyPart;
|
||||
import javax.mail.Flags;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Multipart;
|
||||
import javax.mail.Part;
|
||||
import javax.mail.Session;
|
||||
import javax.mail.internet.ContentType;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
|
||||
@@ -90,7 +92,10 @@ public class MessageHelper {
|
||||
imessage.setRecipients(Message.RecipientType.TO, MessageHelper.decodeAddresses(message.to));
|
||||
|
||||
if (message.cc != null)
|
||||
imessage.setRecipients(Message.RecipientType.CC, MessageHelper.decodeAddresses(message.to));
|
||||
imessage.setRecipients(Message.RecipientType.CC, MessageHelper.decodeAddresses(message.cc));
|
||||
|
||||
if (message.bcc != null)
|
||||
imessage.setRecipients(Message.RecipientType.BCC, MessageHelper.decodeAddresses(message.bcc));
|
||||
|
||||
if (message.subject != null)
|
||||
imessage.setSubject(message.subject);
|
||||
@@ -120,6 +125,10 @@ public class MessageHelper {
|
||||
this.imessage = new MimeMessage(isession, is);
|
||||
}
|
||||
|
||||
boolean getSeen() throws MessagingException {
|
||||
return imessage.isSet(Flags.Flag.SEEN);
|
||||
}
|
||||
|
||||
String getMessageID() throws MessagingException {
|
||||
return imessage.getHeader("Message-ID", null);
|
||||
}
|
||||
@@ -153,11 +162,17 @@ public class MessageHelper {
|
||||
return encodeAddresses(imessage.getRecipients(Message.RecipientType.CC));
|
||||
}
|
||||
|
||||
String getBcc() throws MessagingException, JSONException {
|
||||
return encodeAddresses(imessage.getRecipients(Message.RecipientType.BCC));
|
||||
}
|
||||
|
||||
String getReply() throws MessagingException, JSONException {
|
||||
return encodeAddresses(imessage.getReplyTo());
|
||||
}
|
||||
|
||||
static String encodeAddresses(Address[] addresses) throws JSONException {
|
||||
if (addresses == null)
|
||||
return null;
|
||||
JSONArray jaddresses = new JSONArray();
|
||||
if (addresses != null)
|
||||
for (Address address : addresses)
|
||||
@@ -175,6 +190,8 @@ public class MessageHelper {
|
||||
}
|
||||
|
||||
static Address[] decodeAddresses(String json) {
|
||||
if (json == null)
|
||||
return new Address[0];
|
||||
List<Address> result = new ArrayList<>();
|
||||
try {
|
||||
JSONArray jaddresses = new JSONArray(json);
|
||||
@@ -194,11 +211,9 @@ public class MessageHelper {
|
||||
return result.toArray(new Address[0]);
|
||||
}
|
||||
|
||||
String getHtml() throws MessagingException {
|
||||
return getHtml(imessage);
|
||||
}
|
||||
|
||||
static String getFormattedAddresses(String json) {
|
||||
if (json == null)
|
||||
return null;
|
||||
try {
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (Address address : decodeAddresses(json))
|
||||
@@ -217,10 +232,14 @@ public class MessageHelper {
|
||||
}
|
||||
}
|
||||
|
||||
String getHtml() throws MessagingException {
|
||||
return getHtml(imessage);
|
||||
}
|
||||
|
||||
private String getHtml(Part part) throws MessagingException {
|
||||
if (part.isMimeType("text/*"))
|
||||
try {
|
||||
String s = (String) part.getContent();
|
||||
String s = part.getContent().toString();
|
||||
if (part.isMimeType("text/plain"))
|
||||
s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
|
||||
return s;
|
||||
@@ -267,8 +286,43 @@ public class MessageHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean getSeen() throws MessagingException {
|
||||
return imessage.isSet(Flags.Flag.SEEN);
|
||||
public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
|
||||
List<EntityAttachment> result = new ArrayList<>();
|
||||
|
||||
Object content = imessage.getContent();
|
||||
if (content instanceof String)
|
||||
return result;
|
||||
|
||||
if (content instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) content;
|
||||
for (int i = 0; i < multipart.getCount(); i++)
|
||||
result.addAll(getAttachments(multipart.getBodyPart(i)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<EntityAttachment> getAttachments(BodyPart part) throws IOException, MessagingException {
|
||||
List<EntityAttachment> result = new ArrayList<>();
|
||||
|
||||
Object content = part.getContent();
|
||||
if (content instanceof InputStream || content instanceof String) {
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition()) || !TextUtils.isEmpty(part.getFileName())) {
|
||||
ContentType ct = new ContentType(part.getContentType());
|
||||
EntityAttachment attachment = new EntityAttachment();
|
||||
attachment.sequence = result.size() + 1;
|
||||
attachment.name = part.getFileName();
|
||||
attachment.type = ct.getBaseType();
|
||||
attachment.part = part;
|
||||
result.add(attachment);
|
||||
}
|
||||
} else if (content instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) content;
|
||||
for (int i = 0; i < multipart.getCount(); i++)
|
||||
result.addAll(getAttachments(multipart.getBodyPart(i)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
String getRaw() throws IOException, MessagingException {
|
||||
|
||||
@@ -25,8 +25,12 @@ import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Provider {
|
||||
public String name;
|
||||
@@ -77,6 +81,16 @@ public class Provider {
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
final Collator collator = Collator.getInstance(Locale.getDefault());
|
||||
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
|
||||
|
||||
Collections.sort(result, new Comparator<Provider>() {
|
||||
@Override
|
||||
public int compare(Provider p1, Provider p2) {
|
||||
return collator.compare(p1.name, p2.name);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ import com.sun.mail.imap.protocol.IMAPProtocol;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
@@ -63,6 +66,7 @@ import javax.mail.FetchProfile;
|
||||
import javax.mail.Flags;
|
||||
import javax.mail.Folder;
|
||||
import javax.mail.FolderClosedException;
|
||||
import javax.mail.FolderNotFoundException;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.MessageRemovedException;
|
||||
import javax.mail.MessagingException;
|
||||
@@ -101,7 +105,10 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
|
||||
public ServiceSynchronize() {
|
||||
// https://docs.oracle.com/javaee/6/api/javax/mail/internet/package-summary.html
|
||||
System.setProperty("mail.mime.ignoreunknownencoding", "true");
|
||||
System.setProperty("mail.mime.decodefilename", "true");
|
||||
System.setProperty("mail.mime.encodefilename", "true");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -383,7 +390,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
Log.i(Helper.TAG, account.name + " stopped");
|
||||
}
|
||||
|
||||
private void monitorFolder(final EntityAccount account, final EntityFolder folder, final IMAPStore istore) throws MessagingException, JSONException {
|
||||
private void monitorFolder(final EntityAccount account, final EntityFolder folder, final IMAPStore istore) throws MessagingException, JSONException, IOException {
|
||||
IMAPFolder ifolder = null;
|
||||
try {
|
||||
Log.i(Helper.TAG, folder.name + " start");
|
||||
@@ -498,7 +505,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
};
|
||||
|
||||
// Listen for process operations requests
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
|
||||
lbm.registerReceiver(receiver, new IntentFilter(ACTION_PROCESS_OPERATIONS + folder.id));
|
||||
Log.i(Helper.TAG, folder.name + " listen process id=" + folder.id);
|
||||
try {
|
||||
@@ -545,6 +552,10 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
lbm.unregisterReceiver(receiver);
|
||||
Log.i(Helper.TAG, folder.name + " unlisten process id=" + folder.id);
|
||||
}
|
||||
} catch (FolderNotFoundException ex) {
|
||||
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
|
||||
folder.synchronize = false;
|
||||
DB.getInstance(this).folder().updateFolder(folder);
|
||||
} finally {
|
||||
if (ifolder != null && ifolder.isOpen()) {
|
||||
try {
|
||||
@@ -557,156 +568,151 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
}
|
||||
|
||||
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException {
|
||||
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
|
||||
try {
|
||||
Log.i(Helper.TAG, folder.name + " start process");
|
||||
|
||||
DB db = DB.getInstance(ServiceSynchronize.this);
|
||||
DB db = DB.getInstance(this);
|
||||
DaoOperation operation = db.operation();
|
||||
DaoMessage message = db.message();
|
||||
for (TupleOperationEx op : operation.getOperations(folder.id)) {
|
||||
Log.i(Helper.TAG, folder.name +
|
||||
" Process op=" + op.id + "/" + op.name +
|
||||
" args=" + op.args +
|
||||
" msg=" + op.message);
|
||||
for (TupleOperationEx op : operation.getOperations(folder.id))
|
||||
try {
|
||||
Log.i(Helper.TAG, folder.name +
|
||||
" start op=" + op.id + "/" + op.name +
|
||||
" args=" + op.args +
|
||||
" msg=" + op.message);
|
||||
|
||||
JSONArray jargs = new JSONArray(op.args);
|
||||
|
||||
if (EntityOperation.SEEN.equals(op.name)) {
|
||||
// Mark message (un)seen
|
||||
JSONArray jargs = new JSONArray(op.args);
|
||||
try {
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
if (imessage != null)
|
||||
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
|
||||
else
|
||||
Log.w(Helper.TAG, "Remote message not found uid=" + op.uid);
|
||||
} catch (MessagingException ex) {
|
||||
// Countermeasure
|
||||
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
msg.ui_seen = msg.seen;
|
||||
message.updateMessage(msg);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
} else if (EntityOperation.ADD.equals(op.name)) {
|
||||
// Append message
|
||||
try {
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
Properties props = MessageHelper.getSessionProperties();
|
||||
Session isession = Session.getDefaultInstance(props, null);
|
||||
MimeMessage imessage = MessageHelper.from(msg, isession);
|
||||
|
||||
ifolder.appendMessages(new Message[]{imessage});
|
||||
|
||||
// Draft can be saved multiple times
|
||||
if (msg.uid != null) {
|
||||
Message previously = ifolder.getMessageByUID(msg.uid);
|
||||
previously.setFlag(Flags.Flag.DELETED, true);
|
||||
ifolder.expunge();
|
||||
}
|
||||
|
||||
message.deleteMessage(op.message);
|
||||
} catch (MessagingException ex) {
|
||||
// Countermeasure
|
||||
// TODO: try again?
|
||||
throw ex;
|
||||
}
|
||||
|
||||
} else if (EntityOperation.MOVE.equals(op.name)) {
|
||||
// Move message
|
||||
try {
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
|
||||
Folder target = istore.getFolder(archive.name);
|
||||
|
||||
ifolder.moveMessages(new Message[]{imessage}, target);
|
||||
|
||||
message.deleteMessage(op.message);
|
||||
} catch (MessagingException ex) {
|
||||
// Countermeasure
|
||||
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
msg.ui_hide = false;
|
||||
message.updateMessage(msg);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
} else if (EntityOperation.DELETE.equals(op.name)) {
|
||||
// Delete message
|
||||
try {
|
||||
if (op.uid != null) {
|
||||
if (EntityOperation.SEEN.equals(op.name)) {
|
||||
// Mark message (un)seen
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
if (imessage != null) {
|
||||
imessage.setFlag(Flags.Flag.DELETED, true);
|
||||
ifolder.expunge();
|
||||
} else
|
||||
Log.w(Helper.TAG, "Remote message not found uid=" + op.uid);
|
||||
} else {
|
||||
// Not appended draft
|
||||
Log.w(Helper.TAG, "Delete without uid id=" + op.message);
|
||||
}
|
||||
if (imessage == null)
|
||||
throw new MessageRemovedException();
|
||||
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
|
||||
|
||||
message.deleteMessage(op.message);
|
||||
} catch (MessagingException ex) {
|
||||
// Countermeasure
|
||||
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
msg.ui_hide = false;
|
||||
message.updateMessage(msg);
|
||||
throw ex;
|
||||
}
|
||||
} else if (EntityOperation.ADD.equals(op.name)) {
|
||||
if (!folder.synchronize) {
|
||||
// Local drafts
|
||||
Log.w(Helper.TAG, "Folder synchronization disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (EntityOperation.SEND.equals(op.name)) {
|
||||
// Send message
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
|
||||
EntityIdentity ident = db.identity().getIdentity(msg.identity);
|
||||
// Append message
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
Properties props = MessageHelper.getSessionProperties();
|
||||
Session isession = Session.getDefaultInstance(props, null);
|
||||
MimeMessage imessage = MessageHelper.from(msg, isession);
|
||||
ifolder.appendMessages(new Message[]{imessage});
|
||||
|
||||
try {
|
||||
Properties props = MessageHelper.getSessionProperties();
|
||||
Session isession = Session.getDefaultInstance(props, null);
|
||||
// Drafts can be appended multiple times
|
||||
try {
|
||||
if (msg.uid != null) {
|
||||
Message previously = ifolder.getMessageByUID(msg.uid);
|
||||
previously.setFlag(Flags.Flag.DELETED, true);
|
||||
ifolder.expunge();
|
||||
}
|
||||
} finally {
|
||||
// Remote will report appended
|
||||
message.deleteMessage(op.message);
|
||||
}
|
||||
|
||||
MimeMessage imessage;
|
||||
if (reply == null)
|
||||
imessage = MessageHelper.from(msg, isession);
|
||||
else
|
||||
imessage = MessageHelper.from(msg, reply, isession);
|
||||
if (ident.replyto != null)
|
||||
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
|
||||
|
||||
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
|
||||
try {
|
||||
itransport.connect(ident.host, ident.port, ident.user, ident.password);
|
||||
|
||||
Address[] to = imessage.getRecipients(Message.RecipientType.TO);
|
||||
itransport.sendMessage(imessage, to);
|
||||
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
|
||||
" to " + TextUtils.join(", ", to));
|
||||
|
||||
// Make sure the message is sent only once
|
||||
operation.deleteOperation(op.id);
|
||||
} else if (EntityOperation.MOVE.equals(op.name)) {
|
||||
// Move message
|
||||
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
Folder target = istore.getFolder(archive.name);
|
||||
ifolder.moveMessages(new Message[]{imessage}, target);
|
||||
|
||||
message.deleteMessage(op.message);
|
||||
} finally {
|
||||
itransport.close();
|
||||
}
|
||||
|
||||
} catch (MessagingException ex) {
|
||||
// Countermeasure
|
||||
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
|
||||
EntityFolder drafts = db.folder().getPrimaryDraftFolder();
|
||||
msg.folder = drafts.id;
|
||||
message.updateMessage(msg);
|
||||
// Message will not be sent to remote
|
||||
throw ex;
|
||||
} else if (EntityOperation.DELETE.equals(op.name)) {
|
||||
// Delete message
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
if (imessage == null)
|
||||
throw new MessageRemovedException();
|
||||
imessage.setFlag(Flags.Flag.DELETED, true);
|
||||
ifolder.expunge();
|
||||
|
||||
message.deleteMessage(op.message);
|
||||
|
||||
} else if (EntityOperation.SEND.equals(op.name)) {
|
||||
// Send message
|
||||
EntityMessage msg = message.getMessage(op.message);
|
||||
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
|
||||
EntityIdentity ident = db.identity().getIdentity(msg.identity);
|
||||
|
||||
if (!ident.synchronize) {
|
||||
// Message will remain in outbox
|
||||
return;
|
||||
}
|
||||
|
||||
Properties props = MessageHelper.getSessionProperties();
|
||||
Session isession = Session.getDefaultInstance(props, null);
|
||||
|
||||
MimeMessage imessage;
|
||||
if (reply == null)
|
||||
imessage = MessageHelper.from(msg, isession);
|
||||
else
|
||||
imessage = MessageHelper.from(msg, reply, isession);
|
||||
if (ident.replyto != null)
|
||||
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
|
||||
|
||||
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
|
||||
try {
|
||||
itransport.connect(ident.host, ident.port, ident.user, ident.password);
|
||||
|
||||
Address[] to = imessage.getAllRecipients();
|
||||
itransport.sendMessage(imessage, to);
|
||||
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
|
||||
" to " + TextUtils.join(", ", to));
|
||||
|
||||
// Make sure the message is sent only once
|
||||
operation.deleteOperation(op.id);
|
||||
message.deleteMessage(op.message);
|
||||
} finally {
|
||||
itransport.close();
|
||||
}
|
||||
|
||||
} else if (EntityOperation.ATTACHMENT.equals(op.name)) {
|
||||
int sequence = jargs.getInt(0);
|
||||
EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
|
||||
|
||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||
if (imessage == null)
|
||||
throw new MessageRemovedException();
|
||||
|
||||
Properties props = MessageHelper.getSessionProperties();
|
||||
Session isession = Session.getDefaultInstance(props, null);
|
||||
|
||||
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
|
||||
EntityAttachment a = helper.getAttachments().get(sequence - 1);
|
||||
|
||||
InputStream is = a.part.getInputStream();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
for (int len = is.read(buffer); len != -1; len = is.read(buffer))
|
||||
os.write(buffer, 0, len);
|
||||
|
||||
attachment.content = os.toByteArray();
|
||||
db.attachment().updateAttachment(attachment);
|
||||
Log.i(Helper.TAG, "Downloaded bytes=" + attachment.content.length);
|
||||
|
||||
} else
|
||||
throw new MessagingException("Unknown operation name=" + op.name);
|
||||
|
||||
// Operation succeeded
|
||||
operation.deleteOperation(op.id);
|
||||
|
||||
} catch (MessageRemovedException ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
|
||||
// There is no use in repeating
|
||||
operation.deleteOperation(op.id);
|
||||
}
|
||||
|
||||
} else
|
||||
throw new MessagingException("Unknown operation name=" + op.name);
|
||||
|
||||
operation.deleteOperation(op.id);
|
||||
}
|
||||
} finally {
|
||||
Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
|
||||
}
|
||||
} finally {
|
||||
Log.i(Helper.TAG, folder.name + " end process");
|
||||
}
|
||||
@@ -723,19 +729,19 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
names.add(folder.name);
|
||||
Log.i(Helper.TAG, "Local folder count=" + names.size());
|
||||
|
||||
Folder[] ifolders = istore.getDefaultFolder().list("*");
|
||||
Folder[] ifolders = istore.getDefaultFolder().list("*"); // TODO: is the pattern correct?
|
||||
Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
|
||||
|
||||
for (Folder ifolder : ifolders) {
|
||||
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
|
||||
boolean candidate = true;
|
||||
for (String attr : attrs) {
|
||||
if ("\\Noselect".equals(attr)) {
|
||||
if ("\\Noselect".equals(attr)) { // TODO: is this attribute correct?
|
||||
candidate = false;
|
||||
break;
|
||||
}
|
||||
if (attr.startsWith("\\"))
|
||||
if (EntityFolder.STANDARD_FOLDER_ATTR.contains(attr.substring(1))) {
|
||||
if (EntityFolder.SYSTEM_FOLDER_ATTR.contains(attr.substring(1))) {
|
||||
candidate = false;
|
||||
break;
|
||||
}
|
||||
@@ -765,11 +771,11 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException {
|
||||
private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
|
||||
try {
|
||||
Log.i(Helper.TAG, folder.name + " start sync after=" + folder.after);
|
||||
|
||||
DB db = DB.getInstance(ServiceSynchronize.this);
|
||||
DB db = DB.getInstance(this);
|
||||
DaoMessage dao = db.message();
|
||||
|
||||
// Get reference times
|
||||
@@ -844,57 +850,77 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, JSONException {
|
||||
private void synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, JSONException, IOException {
|
||||
FetchProfile fp = new FetchProfile();
|
||||
fp.add(UIDFolder.FetchProfileItem.UID);
|
||||
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
|
||||
ifolder.fetch(new Message[]{imessage}, fp);
|
||||
|
||||
long uid = ifolder.getUID(imessage);
|
||||
Log.i(Helper.TAG, folder.name + " sync uid=" + uid);
|
||||
try {
|
||||
Log.i(Helper.TAG, folder.name + " start sync uid=" + uid);
|
||||
|
||||
MessageHelper helper = new MessageHelper(imessage);
|
||||
boolean seen = helper.getSeen();
|
||||
if (imessage.isExpunged()) {
|
||||
Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
|
||||
return;
|
||||
}
|
||||
if (imessage.isSet(Flags.Flag.DELETED)) {
|
||||
Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
|
||||
return;
|
||||
}
|
||||
|
||||
DB db = DB.getInstance(ServiceSynchronize.this);
|
||||
EntityMessage message = db.message().getMessage(folder.id, uid);
|
||||
if (message == null) {
|
||||
FetchProfile fp1 = new FetchProfile();
|
||||
fp1.add(FetchProfile.Item.ENVELOPE);
|
||||
fp1.add(FetchProfile.Item.CONTENT_INFO);
|
||||
fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
|
||||
fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
|
||||
ifolder.fetch(new Message[]{imessage}, fp1);
|
||||
MessageHelper helper = new MessageHelper(imessage);
|
||||
boolean seen = helper.getSeen();
|
||||
|
||||
message = new EntityMessage();
|
||||
message.account = folder.account;
|
||||
message.folder = folder.id;
|
||||
message.uid = uid;
|
||||
message.msgid = helper.getMessageID();
|
||||
message.references = TextUtils.join(" ", helper.getReferences());
|
||||
message.inreplyto = helper.getInReplyTo();
|
||||
message.thread = helper.getThreadId(uid);
|
||||
message.from = helper.getFrom();
|
||||
message.to = helper.getTo();
|
||||
message.cc = helper.getCc();
|
||||
message.bcc = null;
|
||||
message.reply = helper.getReply();
|
||||
message.subject = imessage.getSubject();
|
||||
message.body = helper.getHtml();
|
||||
message.received = imessage.getReceivedDate().getTime();
|
||||
message.sent = imessage.getSentDate().getTime();
|
||||
message.seen = seen;
|
||||
message.ui_seen = seen;
|
||||
message.ui_hide = false;
|
||||
DB db = DB.getInstance(this);
|
||||
EntityMessage message = db.message().getMessage(folder.id, uid);
|
||||
if (message == null) {
|
||||
FetchProfile fp1 = new FetchProfile();
|
||||
fp1.add(FetchProfile.Item.ENVELOPE);
|
||||
fp1.add(FetchProfile.Item.CONTENT_INFO);
|
||||
fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
|
||||
fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
|
||||
ifolder.fetch(new Message[]{imessage}, fp1);
|
||||
|
||||
message.id = db.message().insertMessage(message);
|
||||
Log.i(Helper.TAG, folder.name + " added uid=" + uid + " id=" + message.id);
|
||||
} else if (message.seen != seen) {
|
||||
message.seen = seen;
|
||||
message.ui_seen = seen;
|
||||
message = new EntityMessage();
|
||||
message.account = folder.account;
|
||||
message.folder = folder.id;
|
||||
message.uid = uid;
|
||||
message.msgid = helper.getMessageID();
|
||||
message.references = TextUtils.join(" ", helper.getReferences());
|
||||
message.inreplyto = helper.getInReplyTo();
|
||||
message.thread = helper.getThreadId(uid);
|
||||
message.from = helper.getFrom();
|
||||
message.to = helper.getTo();
|
||||
message.cc = helper.getCc();
|
||||
message.bcc = helper.getBcc();
|
||||
message.reply = helper.getReply();
|
||||
message.subject = imessage.getSubject();
|
||||
message.body = helper.getHtml();
|
||||
message.received = imessage.getReceivedDate().getTime();
|
||||
message.sent = imessage.getSentDate().getTime();
|
||||
message.seen = seen;
|
||||
message.ui_seen = seen;
|
||||
message.ui_hide = false;
|
||||
|
||||
db.message().updateMessage(message);
|
||||
Log.i(Helper.TAG, folder.name + " updated uid=" + uid + " id=" + message.id);
|
||||
message.id = db.message().insertMessage(message);
|
||||
Log.i(Helper.TAG, folder.name + " added id=" + message.id);
|
||||
|
||||
for (EntityAttachment attachment : helper.getAttachments()) {
|
||||
Log.i(Helper.TAG, "attachment name=" + attachment.name + " type=" + attachment.type);
|
||||
attachment.message = message.id;
|
||||
db.attachment().insertAttachment(attachment);
|
||||
}
|
||||
|
||||
} else if (message.seen != seen) {
|
||||
message.seen = seen;
|
||||
message.ui_seen = seen;
|
||||
|
||||
db.message().updateMessage(message);
|
||||
Log.i(Helper.TAG, folder.name + " updated id=" + message.id);
|
||||
}
|
||||
} finally {
|
||||
Log.i(Helper.TAG, folder.name + " end sync uid=" + uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ public class TupleMessageEx extends EntityMessage {
|
||||
public String folderType;
|
||||
public int count;
|
||||
public int unseen;
|
||||
public int attachments;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
@@ -32,7 +33,8 @@ public class TupleMessageEx extends EntityMessage {
|
||||
return (super.equals(obj) &&
|
||||
this.folderType.equals(other.folderType) &&
|
||||
this.count == other.count &&
|
||||
this.unseen == other.unseen);
|
||||
this.unseen == other.unseen &&
|
||||
this.attachments == other.attachments);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_black_18.png
Executable file
|
After Width: | Height: | Size: 264 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_black_24.png
Executable file
|
After Width: | Height: | Size: 302 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_black_36.png
Executable file
|
After Width: | Height: | Size: 408 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_black_48.png
Executable file
|
After Width: | Height: | Size: 516 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_white_18.png
Executable file
|
After Width: | Height: | Size: 274 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_white_24.png
Executable file
|
After Width: | Height: | Size: 298 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_white_36.png
Executable file
|
After Width: | Height: | Size: 397 B |
BIN
app/src/main/res/drawable-hdpi/baseline_attachment_white_48.png
Executable file
|
After Width: | Height: | Size: 509 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_black_18.png
Executable file
|
After Width: | Height: | Size: 187 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_black_24.png
Executable file
|
After Width: | Height: | Size: 164 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_black_36.png
Executable file
|
After Width: | Height: | Size: 207 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_black_48.png
Executable file
|
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_white_18.png
Executable file
|
After Width: | Height: | Size: 190 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_white_24.png
Executable file
|
After Width: | Height: | Size: 167 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_white_36.png
Executable file
|
After Width: | Height: | Size: 211 B |
BIN
app/src/main/res/drawable-hdpi/baseline_get_app_white_48.png
Executable file
|
After Width: | Height: | Size: 186 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_black_18.png
Executable file
|
After Width: | Height: | Size: 247 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_black_24.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_black_36.png
Executable file
|
After Width: | Height: | Size: 355 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_black_48.png
Executable file
|
After Width: | Height: | Size: 430 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_white_18.png
Executable file
|
After Width: | Height: | Size: 249 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_white_24.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_white_36.png
Executable file
|
After Width: | Height: | Size: 353 B |
BIN
app/src/main/res/drawable-hdpi/baseline_people_white_48.png
Executable file
|
After Width: | Height: | Size: 425 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_black_18.png
Executable file
|
After Width: | Height: | Size: 166 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_black_24.png
Executable file
|
After Width: | Height: | Size: 205 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_black_36.png
Executable file
|
After Width: | Height: | Size: 302 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_black_48.png
Executable file
|
After Width: | Height: | Size: 389 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_white_18.png
Executable file
|
After Width: | Height: | Size: 171 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_white_24.png
Executable file
|
After Width: | Height: | Size: 209 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_white_36.png
Executable file
|
After Width: | Height: | Size: 298 B |
BIN
app/src/main/res/drawable-mdpi/baseline_attachment_white_48.png
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_black_18.png
Executable file
|
After Width: | Height: | Size: 143 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_black_24.png
Executable file
|
After Width: | Height: | Size: 121 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_black_36.png
Executable file
|
After Width: | Height: | Size: 164 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_black_48.png
Executable file
|
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_white_18.png
Executable file
|
After Width: | Height: | Size: 146 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_white_24.png
Executable file
|
After Width: | Height: | Size: 121 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_white_36.png
Executable file
|
After Width: | Height: | Size: 167 B |
BIN
app/src/main/res/drawable-mdpi/baseline_get_app_white_48.png
Executable file
|
After Width: | Height: | Size: 161 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_black_18.png
Executable file
|
After Width: | Height: | Size: 156 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_black_24.png
Executable file
|
After Width: | Height: | Size: 184 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_black_36.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_black_48.png
Executable file
|
After Width: | Height: | Size: 317 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_white_18.png
Executable file
|
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_white_24.png
Executable file
|
After Width: | Height: | Size: 187 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_white_36.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable-mdpi/baseline_people_white_48.png
Executable file
|
After Width: | Height: | Size: 313 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_black_18.png
Executable file
|
After Width: | Height: | Size: 302 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_black_24.png
Executable file
|
After Width: | Height: | Size: 389 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_black_36.png
Executable file
|
After Width: | Height: | Size: 516 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_black_48.png
Executable file
|
After Width: | Height: | Size: 718 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_white_18.png
Executable file
|
After Width: | Height: | Size: 298 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_white_24.png
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_white_36.png
Executable file
|
After Width: | Height: | Size: 509 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_attachment_white_48.png
Executable file
|
After Width: | Height: | Size: 710 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_black_18.png
Executable file
|
After Width: | Height: | Size: 164 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_black_24.png
Executable file
|
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_black_36.png
Executable file
|
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_black_48.png
Executable file
|
After Width: | Height: | Size: 208 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_white_18.png
Executable file
|
After Width: | Height: | Size: 167 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_white_24.png
Executable file
|
After Width: | Height: | Size: 161 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_white_36.png
Executable file
|
After Width: | Height: | Size: 186 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_get_app_white_48.png
Executable file
|
After Width: | Height: | Size: 208 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_people_black_18.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_people_black_24.png
Executable file
|
After Width: | Height: | Size: 317 B |