mirror of https://github.com/M66B/FairEmail.git
Compare commits
308 Commits
Author | SHA1 | Date |
---|---|---|
M66B | 323da90fc4 | |
M66B | 2b6423c01d | |
M66B | 4f22f269c8 | |
M66B | 325db7faa2 | |
M66B | 194a86f07f | |
M66B | cd14ca4577 | |
M66B | f0f5fd0b31 | |
M66B | 98e31ba411 | |
M66B | d116d72552 | |
M66B | a454f9cbed | |
M66B | f04cbba28d | |
M66B | 0db73c90a0 | |
M66B | 720649957a | |
M66B | 0703bb232d | |
M66B | 93e192a309 | |
M66B | b8d2d72387 | |
M66B | 9726da053c | |
M66B | fbc120d099 | |
M66B | 74a92c9e67 | |
M66B | 9f5593d3d3 | |
M66B | 7453751d12 | |
M66B | b80578bd7a | |
M66B | ff409a6a40 | |
M66B | 5c089b7838 | |
M66B | b51f5b3be0 | |
M66B | e18b6196e7 | |
M66B | 9bedac3787 | |
M66B | eb29775909 | |
M66B | f0a384b714 | |
M66B | 4acbe416d6 | |
M66B | 4935753f72 | |
M66B | d992fe9788 | |
M66B | 4458b24fad | |
M66B | 0155db1a4a | |
M66B | 77d4278aea | |
M66B | b80b223d0c | |
M66B | 9150468a64 | |
M66B | 43b5b8231c | |
M66B | d5349c118a | |
M66B | a2fd648dcf | |
M66B | 79b458f708 | |
M66B | f66effc2e5 | |
M66B | 57d5b42ada | |
M66B | 978e5d0d5e | |
M66B | f1f53f2d5d | |
M66B | 3fe601006a | |
M66B | e0e2ca9ab1 | |
M66B | 648cb4621f | |
M66B | 8f97864131 | |
M66B | 153bf8b577 | |
M66B | 1489dcb775 | |
M66B | a25ab1367f | |
M66B | 1083865e77 | |
M66B | 00ab42f973 | |
M66B | 5a65365f2c | |
M66B | a06e4aa0a3 | |
M66B | b1d0aa4ec6 | |
M66B | 4d27446860 | |
M66B | c9f22fbf97 | |
M66B | 3546ab9cc7 | |
M66B | 7bdac5b902 | |
M66B | f79b1579f9 | |
M66B | 9173ec038c | |
M66B | 68a95a4f90 | |
M66B | 43d3c3804e | |
M66B | 12f4a44239 | |
M66B | ebfb64d714 | |
M66B | 2cc73a7927 | |
M66B | 2aa8f93f5d | |
M66B | b3f455ff7c | |
M66B | 127128db19 | |
M66B | cffbad0d0d | |
M66B | 1a9962072d | |
M66B | 6f925548e0 | |
M66B | 5f188ef0f3 | |
M66B | 0378df51d7 | |
M66B | 62bb023103 | |
M66B | 60721c8139 | |
M66B | 77ccdd48c3 | |
M66B | 3a9b56438d | |
M66B | a9efded85c | |
M66B | 22ae019243 | |
M66B | edaedbfe7d | |
M66B | 0244ef3f82 | |
M66B | 40daebe326 | |
M66B | 63e8bd160c | |
M66B | 73097569f5 | |
M66B | 32964f1edd | |
M66B | dc63ec78fd | |
M66B | 10211970fb | |
M66B | 252ad4c2a5 | |
M66B | 229a45d87b | |
M66B | 6bdfffcce5 | |
M66B | db4a298b3c | |
M66B | c86802c380 | |
M66B | f86b572eae | |
M66B | 08d845887c | |
M66B | a5a5cadafa | |
M66B | 53090f2f44 | |
M66B | fef76265be | |
M66B | 82c64f3171 | |
M66B | ae25f0017c | |
M66B | 3f06073664 | |
M66B | a0775944a1 | |
M66B | 8733388ce7 | |
M66B | 5c7f366e30 | |
M66B | 20f9d67b88 | |
M66B | f7148dad57 | |
M66B | 90e527cb60 | |
M66B | c4442cccf3 | |
M66B | 1cbebf7870 | |
M66B | 4a52109212 | |
M66B | f4c2025c39 | |
M66B | 34342bc96b | |
M66B | 0b66c0a45e | |
M66B | caef8803f2 | |
M66B | 6a9091d596 | |
M66B | a906750374 | |
M66B | 94b514d58f | |
M66B | 29ac27aa45 | |
M66B | 45267c0f72 | |
M66B | e8e72d03d2 | |
M66B | 413964da62 | |
M66B | a313268b1c | |
M66B | 5bc84a0482 | |
M66B | e6dff96061 | |
M66B | 5b210a238e | |
M66B | a805164231 | |
M66B | 048c558ebd | |
M66B | a88379a09d | |
M66B | 01621724cd | |
M66B | 7659fde011 | |
M66B | 4918e87ff7 | |
M66B | 0a4c7ae54f | |
M66B | 35d5324c1b | |
M66B | b86420ad9d | |
M66B | e312b06a10 | |
M66B | a74fefee45 | |
M66B | 1ec195d1eb | |
M66B | 3ed031cfa3 | |
M66B | 7ac91b3497 | |
M66B | 77044946e7 | |
M66B | e9937c7308 | |
M66B | 2634ed276b | |
M66B | 4836366b48 | |
M66B | 77c56905d9 | |
M66B | 0385883390 | |
M66B | be73157316 | |
M66B | 384249a248 | |
M66B | 5a44e99a06 | |
M66B | 944a2a9a09 | |
M66B | 1e483b7438 | |
M66B | f2882193d7 | |
M66B | 3a0083ad44 | |
M66B | 8cb5a52ae7 | |
M66B | 18c85872f3 | |
M66B | 3d367c429f | |
M66B | 43d89b6c13 | |
M66B | 3300463e79 | |
M66B | 93fc9b169c | |
M66B | 04fe7b3743 | |
M66B | 1fad4ad80c | |
M66B | b20b7dee10 | |
M66B | a207b73082 | |
M66B | 12e79ce7e9 | |
M66B | bfa347aa71 | |
M66B | e93c0de346 | |
M66B | 8cab3786e9 | |
M66B | 861716aad0 | |
M66B | a2b05e420d | |
M66B | a54f0f3f03 | |
M66B | 4442619c73 | |
M66B | 497ce2fda6 | |
M66B | 8a4e6aa939 | |
M66B | 18d69965ef | |
M66B | 89b2b1f88c | |
M66B | 3b1a31621f | |
M66B | 3994216cc5 | |
M66B | df5fbe9f35 | |
M66B | cd07035e71 | |
M66B | 458d52353e | |
M66B | 8d7fb057fd | |
M66B | d83dde8abc | |
M66B | 096077d8b7 | |
M66B | 3aaf7ba072 | |
M66B | dfc871007f | |
M66B | 0157efb61d | |
M66B | b56045b66e | |
M66B | 0691568835 | |
M66B | 3b11de947c | |
M66B | f9cc6bdae1 | |
M66B | 2154dd261e | |
M66B | f3e586b112 | |
M66B | 1cd05f1fd1 | |
M66B | 4a21502651 | |
M66B | 4bb1284c52 | |
M66B | 374f121d5f | |
M66B | 864c522ee5 | |
M66B | c8965a64a8 | |
M66B | 5382a2580f | |
M66B | 69dcb2b9e8 | |
M66B | 2c2e35bc0a | |
M66B | 5dcc52424c | |
M66B | 951a26d7b3 | |
M66B | 56d566e292 | |
M66B | 932e1513ac | |
M66B | a88ceaf533 | |
M66B | dc5d26151a | |
M66B | 07f6fa6095 | |
M66B | 03a331175f | |
M66B | 8f2bab4c48 | |
M66B | b8250fe6a0 | |
M66B | 58a63a366d | |
M66B | d636a73bc3 | |
M66B | a8f1ba0705 | |
M66B | 794cb9b27c | |
M66B | ea03f8c5f6 | |
M66B | 0f7283f9ec | |
M66B | 04265c6a7d | |
M66B | fb02b318e0 | |
M66B | 5f4c1f1ce1 | |
M66B | c131de5f0e | |
M66B | 8c8337270a | |
M66B | 563f2a1951 | |
M66B | 13ff382ba4 | |
M66B | d9b06148b0 | |
M66B | 600f8c0b00 | |
M66B | b8b36604d8 | |
M66B | 5b547b6ba0 | |
M66B | 168484ddb5 | |
M66B | ad53d3d1ea | |
M66B | efff870d97 | |
M66B | 21defedb76 | |
M66B | 0aa628dc99 | |
M66B | 8270c9abbf | |
M66B | 7c8548616a | |
M66B | 97e30f3a71 | |
M66B | ebd9003634 | |
M66B | ce0cc44025 | |
M66B | 5d16bd8d7d | |
M66B | 700f3740f9 | |
M66B | d57eae01f2 | |
M66B | 10b736c2e7 | |
M66B | 46ff530412 | |
M66B | 3989504073 | |
M66B | caab02b0d6 | |
M66B | 4995900afc | |
M66B | 5c161ed230 | |
M66B | e48326ba44 | |
M66B | c5b6a3e8dc | |
M66B | ab36faa82b | |
M66B | 1840bc0fdd | |
M66B | e46c9cbd47 | |
M66B | aab3de3dd2 | |
M66B | 18df2e7d0d | |
M66B | f902e252f0 | |
M66B | bb442b14c0 | |
M66B | b37d97c516 | |
M66B | 957276c1d4 | |
M66B | a18199e420 | |
M66B | ee110b35ed | |
M66B | 855b9d56da | |
M66B | 15caf283d3 | |
M66B | 6edffb9a61 | |
M66B | 1a9748191b | |
M66B | 93835d886b | |
M66B | 98326a520e | |
M66B | 253ded0be4 | |
M66B | ebc5800703 | |
M66B | 99a88bb14e | |
M66B | 21ac49f1c4 | |
M66B | 2589a5003f | |
M66B | 9b85d39460 | |
M66B | eef306cf3c | |
M66B | f20b4c978d | |
M66B | dd7f3d8f4d | |
M66B | 765d06241d | |
M66B | cf136ec762 | |
M66B | 70455dcf17 | |
M66B | 6bff3e1349 | |
M66B | dd6e85cee2 | |
M66B | fdb1dcfd52 | |
M66B | 48913a1a3e | |
M66B | b48532f8bd | |
M66B | 2a1b403636 | |
M66B | 178553b0f6 | |
M66B | 6f399b39ad | |
M66B | a0098357a8 | |
M66B | 8529e6c012 | |
M66B | a32d71273b | |
M66B | 3401f301ad | |
M66B | 9fb426303b | |
M66B | 3ebc257728 | |
M66B | 9787c379ef | |
M66B | 028ffba4ab | |
M66B | bddae1af77 | |
M66B | 4569ff5b2b | |
M66B | 26ee785be3 | |
M66B | 0ebfbc584c | |
M66B | 1bb28fc4e4 | |
M66B | 3f106c4d30 | |
M66B | 069064502c | |
M66B | 0b44482f5b | |
M66B | 7a066d774c | |
M66B | 433790bbd0 | |
M66B | 265d5d1299 | |
M66B | be4802bccc | |
M66B | c0e2a360ee |
|
@ -7,7 +7,10 @@ on:
|
|||
password:
|
||||
description: 'Password'
|
||||
required: true
|
||||
|
||||
branch:
|
||||
description: 'Branch'
|
||||
required: true
|
||||
default: 'master'
|
||||
jobs:
|
||||
build:
|
||||
|
||||
|
@ -17,6 +20,8 @@ jobs:
|
|||
#https://github.com/actions/setup-java
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
|
@ -49,9 +54,9 @@ jobs:
|
|||
run: ./gradlew assembleGithubRelease assembleLargeRelease assemblePlayRelease uploadBugsnagGithub-releaseMapping uploadBugsnagLarge-releaseMapping uploadBugsnagPlay-releaseMapping
|
||||
- name: Upload to BitBucket
|
||||
run: |
|
||||
./gradlew upload -Ptarget=play-preview
|
||||
./gradlew upload -Ptarget=github-snapshot
|
||||
./gradlew upload -Ptarget=large-snapshot
|
||||
./gradlew upload -Ptarget=play-preview-${{ github.event.inputs.branch }}
|
||||
./gradlew upload -Ptarget=github-snapshot-${{ github.event.inputs.branch }}
|
||||
./gradlew upload -Ptarget=large-snapshot-${{ github.event.inputs.branch }}
|
||||
#https://github.com/actions/upload-artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
|
@ -17,6 +17,7 @@ crowdin.properties
|
|||
.idea/vcs.xml
|
||||
.idea/workspace.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/deploymentTargetSelector.xml
|
||||
app/.cxx/
|
||||
build
|
||||
captures
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.22" />
|
||||
<option name="version" value="1.9.23" />
|
||||
</component>
|
||||
</project>
|
79
CHANGELOG.md
79
CHANGELOG.md
|
@ -4,9 +4,80 @@
|
|||
|
||||
For support you can use [the contact form](https://contact.faircode.eu/?product=fairemailsupport).
|
||||
|
||||
### [Acantholipan](https://en.wikipedia.org/wiki/Acantholipan)
|
||||
|
||||
### Next version
|
||||
|
||||
* Prepared for Android 15
|
||||
* Added "AI" summarize rule action
|
||||
* Listing NOT rule conditions
|
||||
* Reverted AndroidX fragment to version 1.6.2
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
Preview versions are available [here](https://bitbucket.org/M66B/fairemail-test/downloads/).
|
||||
|
||||
### 1.2182 - 2024-05-15
|
||||
|
||||
* Added optional "AI" summarize quick action
|
||||
* Added optional "AI" summarize swipe action
|
||||
* Changed default OpenAI model to [gpt-4o](https://openai.com/index/hello-gpt-4o/)
|
||||
* Improved OpenAI integration (added multimodal support)
|
||||
* Improved Gemini integration
|
||||
* Made "AI" integrations available in the Play Store version
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
*The use of "AI" integrations is and will remain completely optional*
|
||||
|
||||
### 1.2181 - 2024-05-13
|
||||
|
||||
* Reverted [AndroidX ROOM](https://developer.android.com/jetpack/androidx/releases/room#2.6.1) to version 2.4.3 to solve locking issues (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2180 - 2024-05-13
|
||||
|
||||
* Improved [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
|
||||
* Performance improvements
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [NDK](https://developer.android.com/ndk/)
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2179 - 2024-05-08
|
||||
|
||||
* Added option to change "AI" summarize prompt
|
||||
* Added expression condition functions, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated build tools
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated libraries (including [Bouncy Castle](https://www.bouncycastle.org/))
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2178 - 2024-04-29
|
||||
|
||||
* Added "AI" summarization of received messages (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sup>(*) Via the horizontal three-dots button just above the message text. ChatGPT or Gemini needs to be configured in the integrations-settings tab page for this.</sub>
|
||||
|
||||
### 1.2177 - 2024-04-27
|
||||
|
||||
* Added [Have I Been Pwned?](https://haveibeenpwned.com/) **<ins>password</ins>** check (*)
|
||||
* Added identity option to configure envelope-from (*MAIL FROM*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sub>(*) Via the three-dots overflow menu of the account list under "*Manual setup and account options*" in the main settings screen (GitHub version only)</sub>
|
||||
|
||||
### [Zby](https://en.wikipedia.org/wiki/Zby)
|
||||
|
||||
### 1.2176 - 2024-04-22
|
||||
### 1.2176 - 2024-04-22 *
|
||||
|
||||
* Fixed British English translation
|
||||
* Small improvements and minor bug fixes
|
||||
|
@ -39,7 +110,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
(*) Currently supported email forwarders:
|
||||
<sub>(*) Currently supported email forwarders:</sub>
|
||||
|
||||
* [addy.io](https://addy.io/)
|
||||
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
|
||||
|
@ -231,6 +302,8 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Updated libraries (Apache Compress, Bugsnag, Bouncy Castle, Jsoup)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<!-- truncate here -->
|
||||
|
||||
### 1.2145 - 2023-12-30
|
||||
|
||||
* Added Adguard filter list to remove tracking parameters from links, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq200)
|
||||
|
@ -1641,7 +1714,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Small improvements and minor bug fixes
|
||||
* Updated translations
|
||||
|
||||
(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required
|
||||
<sub>(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required</sub>
|
||||
|
||||
### 1.1930 - 2022-07-04
|
||||
|
||||
|
|
94
FAQ.md
94
FAQ.md
|
@ -550,6 +550,8 @@ The low priority status bar notification shows the number of pending operations,
|
|||
* *rule*: execute rule on body text
|
||||
* *expunge*: permanently delete messages
|
||||
* *report*: process delivery or read receipt (experimental)
|
||||
* *download*: async download of text and attachments (experimental)
|
||||
* *subject*: update subject
|
||||
|
||||
Operations are processed only when there is a connection to the email server or when manually synchronizing.
|
||||
See also [this FAQ](#faq16).
|
||||
|
@ -709,12 +711,13 @@ If you use the Play store or GitHub version of FairEmail,
|
|||
you can use the quick setup wizard to easily setup a Gmail account and identity.
|
||||
The Gmail quick setup wizard is not available for third party builds, like the F-Droid build
|
||||
because Google approved the use of OAuth for official builds only.
|
||||
OAuth is also not available on devices without Google services, such as recent Huawei devices, in which case selecting an account will fail.
|
||||
|
||||
The Gmail quick setup wizard won't work if the Android account manager doesn't work or doesn't support Google accounts,
|
||||
When using OAuth with multiple Google accounts, other Google accounts probably need to be logged out first.
|
||||
|
||||
The "*Gmail (Android)*" quick setup wizard won't work if the Android account manager doesn't work or doesn't support Google accounts,
|
||||
which is typically the case if the account selection is being *canceled* right away.
|
||||
|
||||
If you don't want to use or can't use an on-device Google account, for example on recent Huawei devices,
|
||||
If you don't want to use or can't use OAuth or an on-device Google account, for example on recent Huawei devices,
|
||||
you can ~~either enable access for "less secure apps" and use your account password (not advised)~~
|
||||
or enable two factor authentication and use an app specific password.
|
||||
To use a password you can use the quick setup wizard and select *Other provider*.
|
||||
|
@ -912,7 +915,9 @@ POP3 is supported!
|
|||
|
||||
Communication with email servers is always encrypted, unless you explicitly turned this off.
|
||||
This question is about optional end-to-end encryption with PGP or S/MIME.
|
||||
|
||||
The sender and recipient should first agree on this and exchange signed messages to transfer their public key to be able to send encrypted messages.
|
||||
There is a gesture icon button just above the text of a received message on the right to verify a signature and store the public key.
|
||||
|
||||
<br />
|
||||
|
||||
|
@ -1472,6 +1477,12 @@ Sometimes it is necessary to enable external access (IMAP/SMTP) on the website o
|
|||
Other possible causes are that the account is blocked or that logging in has been administratively restricted in some way,
|
||||
for example by allowing to login from certain networks / IP addresses only.
|
||||
|
||||
<br />
|
||||
|
||||
**In the case of an existing account with an authentication error, please try to use the quick setup wizard button to authenticate the account again.**
|
||||
|
||||
<br />
|
||||
|
||||
* **Free.fr**: please see [this FAQ](#faq157)
|
||||
* **Gmail / G suite**: please see [this FAQ](#faq6)
|
||||
* **iCloud**: please see [this FAQ](#faq148)
|
||||
|
@ -2384,6 +2395,8 @@ Then use the three dot action button to execute the desired action.
|
|||
There are almost no providers offering the [JMAP](https://jmap.io/) protocol,
|
||||
so it is not worth a lot of effort to add support for this to FairEmail.
|
||||
|
||||
Moreover, the only available [Java JMAP library](https://github.com/iNPUTmice/jmap) seems not to be maintained anymore.
|
||||
|
||||
<br />
|
||||
|
||||
<a name="faq57"></a>
|
||||
|
@ -2749,30 +2762,44 @@ Note that a regular expression supports an *or* operator, so if you want to matc
|
|||
Note that [dot all mode](https://developer.android.com/reference/java/util/regex/Pattern#DOTALL) is enabled
|
||||
to be able to match [unfolded headers](https://tools.ietf.org/html/rfc2822#section-3.2.3).
|
||||
|
||||
<br />
|
||||
|
||||
**Expressions**
|
||||
|
||||
Since version 1.2174 it is possible to use expression conditions, which is [experimental](#faq125) for now.
|
||||
|
||||
Please [see here](https://ezylang.github.io/EvalEx/references/references.html) about which constants, operators and functions are available.
|
||||
|
||||
The following extra variables are available:
|
||||
|
||||
* from (array)
|
||||
* to (array)
|
||||
* subject
|
||||
* text
|
||||
* hasAttachments
|
||||
* *received* (long, unix epoch in milliseconds; since version 1.2179)
|
||||
* *from* (array of strings)
|
||||
* *to* (array of strings)
|
||||
* *subject* (string)
|
||||
* *text* (string)
|
||||
* *hasAttachments* (boolean; deprecated, use function *attachments()* instead)
|
||||
|
||||
The following extra operators are available:
|
||||
|
||||
* contains
|
||||
* matches (=regex)
|
||||
* *contains* (string/array of strings contains substring)
|
||||
* *matches* (string/array of strings matches regex)
|
||||
|
||||
The following extra functions are available:
|
||||
|
||||
* header(name)
|
||||
* *header(name)* (returns an array of header values for the named header)
|
||||
* *blocklist()* (version 1.2176-1.2178; deprecated, use *onBlocklist()* instead)
|
||||
* *onBlocklist()* (returns a boolean indicating if the sender/server is on a DNS blocklist; since version 1.2179)
|
||||
* *hasMx()* (returns a boolean indicating if the from/reply-to address has an associated MX record; since version 1.2179)
|
||||
* *attachments()* (returns an integer indicating number of attachments; since version 1.2179)
|
||||
* *Jsoup()* (returns an array of selected strings; since version 1.2179)
|
||||
* *Size(array)* (returns the number of items in an array; since version 1.2179)
|
||||
* *knownContact()* (returns a boolean indicating that the from/reply-to address is in the Android address book or in the local contacts database)
|
||||
|
||||
Example condition:
|
||||
Example conditions:
|
||||
|
||||
```header("X-Mailer") contains "Open-Xchange" && from matches ".*service@.*" && !hasAttachments```
|
||||
```header("X-Mailer") contains "Open-Xchange" && from matches ".*service@.*"```
|
||||
|
||||
```!onBlocklist() && hasMx() && attachments() > 0```
|
||||
|
||||
<br>
|
||||
|
||||
|
@ -5547,6 +5574,10 @@ Cloud sync is an experimental feature. It is not available for the Play Store ve
|
|||
|
||||
OpenAI can only be used if configured and enabled.
|
||||
|
||||
**Note that using OpenAI is not free (anymore) !**
|
||||
|
||||
<br>
|
||||
|
||||
**Setup**
|
||||
|
||||
* Create an account [here](https://platform.openai.com/signup)
|
||||
|
@ -5558,6 +5589,8 @@ OpenAI can only be used if configured and enabled.
|
|||
|
||||
**Usage**
|
||||
|
||||
*Editor*
|
||||
|
||||
Tap on the robot button in the top action bar of the message editor.
|
||||
The text in the message editor (if any) and the first part of the message being replied to (if any)
|
||||
will be used for [chat completion](https://platform.openai.com/docs/guides/chat/introduction).
|
||||
|
@ -5568,25 +5601,32 @@ For example: create a new draft and enter the text "*How far is the sun?*", and
|
|||
|
||||
<br>
|
||||
|
||||
*Summarize* (since version 1.2178)
|
||||
|
||||
You can request a summary via the horizontal three-dots button just above the message text.
|
||||
It is possible to configure a button for this (a robot icon).
|
||||
|
||||
The summary prompt text can be configured in the receive-settings tab page.
|
||||
The default is *Summarize the following text:*.
|
||||
|
||||
Since version 1.2182 is is possible to configure a quick action button and a swipe action to summarize a message.
|
||||
|
||||
<br>
|
||||
|
||||
OpenAI isn't very fast, so be patient. Sometimes a timeout error occurs because the app is not receiving a response from OpenAI.
|
||||
|
||||
<br>
|
||||
|
||||
Depending on the ChatGPT account (free or paid) there are usage limits. If you exceed the limit, there will be an error message like this:
|
||||
If you exceed [your usage limit](https://platform.openai.com/docs/guides/rate-limits), there will be an error message like this:
|
||||
|
||||
*Error 429: Too Many Requests insufficient_quota: You exceeded your current quota, please check your plan and billing details*
|
||||
|
||||
In this case, you'll either need to wait, or upgrade your ChatGPT plan.
|
||||
Please [see here](https://help.openai.com/en/articles/6843909-rate-limits-and-429-too-many-requests-errors) for details.
|
||||
Note that you are required to switch to a [paid plan](https://openai.com/api/pricing/) after the testing period.
|
||||
|
||||
<br>
|
||||
|
||||
You can select the [model](https://platform.openai.com/docs/models/overview),
|
||||
configure the [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature)
|
||||
and enable [moderation](https://platform.openai.com/docs/api-reference/moderations) in the integration settings.
|
||||
|
||||
If you have access to GPT-4, you can change the model name to [gpt-4](https://platform.openai.com/docs/models/gpt-4) in the integration settings.
|
||||
There is currently a [waitlist](https://openai.com/waitlist/gpt-4-api) for API GPT-4 access.
|
||||
and configure the [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature).
|
||||
|
||||
Please read the [privacy policy](https://openai.com/policies/privacy-policy) of OpenAI,
|
||||
and perhaps [this article](https://katedowninglaw.com/2023/03/10/openais-massive-data-grab/)
|
||||
|
@ -5603,7 +5643,7 @@ It is possible to use **DeepInfra** too (since version 1.2132).
|
|||
|
||||
<br>
|
||||
|
||||
This feature is experimental and available in the GitHub version only and requires version 1.2053 or later.
|
||||
This feature is experimental and requires version 1.2053 or later for the GitHub version and version 1.2182 or later for the Play Store version.
|
||||
|
||||
<br>
|
||||
|
||||
|
@ -5851,6 +5891,12 @@ Basically, an outgoing message is either in the draft messages folder, the outbo
|
|||
<a name="faq204"></a>
|
||||
**(204) How do I use Gemini?**
|
||||
|
||||
Gemini can only be used if configured and enabled.
|
||||
|
||||
**Note that using Gemini is not free (anymore) !**
|
||||
|
||||
<br>
|
||||
|
||||
To use [Gemini](https://gemini.google.com/), please follow these steps:
|
||||
|
||||
1. Check if your country [is supported](https://ai.google.dev/available_regions)
|
||||
|
@ -5858,12 +5904,14 @@ To use [Gemini](https://gemini.google.com/), please follow these steps:
|
|||
1. Enter the API key in the integration settings tab page
|
||||
1. Enable Gemini integration in the integration settings tab page
|
||||
|
||||
<br>
|
||||
|
||||
For usage instructions, please see [this FAQ](#faq190).
|
||||
|
||||
Please read the privacy policy of [Gemini](https://support.google.com/gemini/answer/13594961).
|
||||
FairEmail does not use third-party libraries to avoid being tracked when Gemini is not being used.
|
||||
|
||||
This feature is experimental and available in the GitHub version only and requires version 1.2171 or later.
|
||||
This feature is experimental and requires version 1.2171 or later for the GitHub version and version 1.2182 or later for the Play Store version.
|
||||
|
||||
<br>
|
||||
|
||||
|
|
17
PRIVACY.md
17
PRIVACY.md
|
@ -37,7 +37,7 @@ FairEmail **does not** send account information and message data elsewhere than
|
|||
FairEmail **does not** allow other apps access to message data without your approval.
|
||||
|
||||
FairEmail **does not** require unnecessary permissions.
|
||||
For more information on permissions, see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq1).
|
||||
For more information on permissions, see [this FAQ](https://m66b.github.io/FairEmail/#faq1).
|
||||
|
||||
FairEmail **does** use modern and secure transport protocols by default.
|
||||
|
||||
|
@ -47,7 +47,7 @@ FairEmail **does** follow the recommendations of [this EFF article](https://www.
|
|||
|
||||
FairEmail is 100 % **open source**, see [the license](https://github.com/M66B/FairEmail/blob/master/LICENSE).
|
||||
|
||||
Error reporting via Bugsnag **is opt-in**, see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) for more information.
|
||||
Error reporting via Bugsnag **is opt-in**, see [here](https://m66b.github.io/FairEmail/#faq104) for more information.
|
||||
|
||||
FairEmail **adheres** to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy),
|
||||
including the [Limited Use requirements](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes).
|
||||
|
@ -69,10 +69,13 @@ FairEmail **can use** these services if they are explicitly enabled (off by defa
|
|||
* [DeepL](https://www.deepl.com/) – [Privacy policy](https://www.deepl.com/privacy/)
|
||||
* [LanguageTool](https://languagetool.org/) – [Privacy policy](https://languagetool.org/legal/privacy)
|
||||
* [VirusTotal](https://www.virustotal.com/) – [Privacy policy](https://support.virustotal.com/hc/en-us/articles/115002168385-Privacy-Policy)
|
||||
* [OpenAI](https://openai.com/) (GitHub version only) – [Privacy policy](https://openai.com/policies/privacy-policy)
|
||||
* [OpenAI](https://openai.com/) – [Privacy policy](https://openai.com/policies/privacy-policy)
|
||||
* [Google Gemini](https://gemini.google.com/) – [Privacy policy](https://support.google.com/gemini/answer/13594961)
|
||||
* [Gravatar](https://gravatar.com/) (GitHub version only) – [Privacy policy](https://automattic.com/privacy/)
|
||||
* [Libravatar](https://www.libravatar.org/) (GitHub version only) – [Privacy policy](https://www.libravatar.org/privacy/)
|
||||
* [GitHub](https://github.com/) (GitHub version only) – [Privacy policy](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement)
|
||||
* [Have I Been Pwned?](https://haveibeenpwned.com/) – [Privacy policy](https://haveibeenpwned.com/Privacy)
|
||||
* [Bugsnag](https://www.bugsnag.com/) – [Privacy policy](https://smartbear.com/privacy/)
|
||||
|
||||
FairEmail **can access** the websites at the domain names of email addresses (username@domain.name)
|
||||
if [Brand Indicators for Message Identification](https://en.wikipedia.org/wiki/Brand_Indicators_for_Message_Identification) (BIMI)
|
||||
|
@ -105,12 +108,14 @@ This table provides a complete overview of all shared data and the conditions un
|
|||
| LanguageTool | Entered message texts | If LanguageTools is enabled, upon long pressing the save draft button |
|
||||
| VirusTotal | [SHA-256 hash](https://en.wikipedia.org/wiki/SHA-2) of attachments | If VirusTotal is enabled, upon long pressing a scan button (*) |
|
||||
| VirusTotal | Attached file contents | If VirusTotal is enabled, upon long pressing an upload button (*) |
|
||||
| OpenAI | Received and entered message texts | Upen pressing a button in a navigation bar (*) |
|
||||
| OpenAI/ChatGPT | Received and entered message texts | If configured and upon pressing a button or using a menu item |
|
||||
| Google Gemini | Received and entered message texts | If configured and upon pressing a button or using a menu item |
|
||||
| Gravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Gravatars are enabled, upon receiving a message (*) |
|
||||
| Libravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Libravatars are enabled, upon receiving a message (*) |
|
||||
| GitHub | None, but see the remarks below | Upon downloading AdGuard tracking parameter list |
|
||||
| | | Upon downloading Disconnect's Tracker Protection lists |
|
||||
| | | Upon checking for updates (*) |
|
||||
| Have I Been Pwned? | The first 5 characters of the SHA1 hash of passwords | Upon checking for being pwned |
|
||||
| BIMI | Domain name of email addresses | If BIMI is enabled, upon receiving a message (*) |
|
||||
| Favicons | Domain name of email addresses | If favicons are enabled, upon receiving a message |
|
||||
| Link title | Link address | Upon pressing a download button in the insert link dialog |
|
||||
|
@ -193,13 +198,13 @@ The sub-processors are:
|
|||
#### V. Permissions
|
||||
|
||||
The app only requests permissions that are necessary for the expected behavior of an email app.
|
||||
For more information on permissions, see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq1).
|
||||
For more information on permissions, see [this FAQ](https://m66b.github.io/FairEmail/#faq1).
|
||||
|
||||
#### VI. Logging
|
||||
|
||||
The app does not send any log entries to the data processor by default.
|
||||
The error reporting system utilizes Bugsnag and is disabled by default.
|
||||
See [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) for more information.
|
||||
See [this FAQ](https://m66b.github.io/FairEmail/#faq104) for more information.
|
||||
|
||||
#### VII. Legal basis
|
||||
|
||||
|
|
36
README.md
36
README.md
|
@ -57,7 +57,7 @@ This app starts a foreground service with a low-priority status bar notification
|
|||
* Confirm showing images to prevent tracking
|
||||
* Confirm opening links to prevent tracking and phishing
|
||||
* Attempt to recognize and disable tracking images
|
||||
* Warning if messages could not be [authenticated](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq92)
|
||||
* Warning if messages could not be [authenticated](https://m66b.github.io/FairEmail/#faq92)
|
||||
|
||||
## Simple
|
||||
|
||||
|
@ -74,7 +74,7 @@ This app starts a foreground service with a low-priority status bar notification
|
|||
* Confirm opening links, images and attachments
|
||||
* No special permissions required
|
||||
* No advertisements
|
||||
* No analytics and no tracking ([error reporting](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) via Bugsnag is opt-in)
|
||||
* No analytics and no tracking ([error reporting](https://m66b.github.io/FairEmail/#faq104) via Bugsnag is opt-in)
|
||||
* No [Google backup](https://developer.android.com/guide/topics/data/backup)
|
||||
* No [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging)
|
||||
* FairEmail is an original work, not a fork or a clone
|
||||
|
@ -90,21 +90,21 @@ This app starts a foreground service with a low-priority status bar notification
|
|||
All pro features are convenience or advanced features.
|
||||
|
||||
* Account/identity/folder colors
|
||||
* Colored stars ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq107))
|
||||
* Notification settings (sounds) per account/folder/sender (requires Android 8 Oreo) ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq145))
|
||||
* Colored stars ([instructions](https://m66b.github.io/FairEmail/#faq107))
|
||||
* Notification settings (sounds) per account/folder/sender (requires Android 8 Oreo) ([instructions](https://m66b.github.io/FairEmail/#faq145))
|
||||
* Configurable notification actions
|
||||
* Snooze messages ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq67))
|
||||
* Snooze messages ([instructions](https://m66b.github.io/FairEmail/#faq67))
|
||||
* Send messages after selected time
|
||||
* Synchronization scheduling ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq78))
|
||||
* Reply templates ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq179))
|
||||
* Synchronization scheduling ([instructions](https://m66b.github.io/FairEmail/#faq78))
|
||||
* Reply templates ([instructions](https://m66b.github.io/FairEmail/#faq179))
|
||||
* Accept/decline calendar invitations
|
||||
* Add message to calendar
|
||||
* Automatically generate vCard attachments
|
||||
* Filter rules ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71))
|
||||
* Automatic message classification ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq163))
|
||||
* Search indexing ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq13))
|
||||
* S/MIME sign/encrypt ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq12))
|
||||
* Biometric/PIN authentication ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq113))
|
||||
* Filter rules ([instructions](https://m66b.github.io/FairEmail/#faq71))
|
||||
* Automatic message classification ([instructions](https://m66b.github.io/FairEmail/#faq163))
|
||||
* Search indexing ([instructions](https://m66b.github.io/FairEmail/#faq13))
|
||||
* S/MIME sign/encrypt ([instructions](https://m66b.github.io/FairEmail/#faq12))
|
||||
* Biometric/PIN authentication ([instructions](https://m66b.github.io/FairEmail/#faq113))
|
||||
* Message list widget
|
||||
* Export settings
|
||||
|
||||
|
@ -123,10 +123,10 @@ Supported download locations:
|
|||
* ~~[AppGallery](https://wap3.hispace.hicloud.com/uowap/index.jsp#/detailApp/C101678151) (the AppGallery app can be downloaded [here](https://huaweimobileservices.com/appgallery/))~~
|
||||
* ~~[Amazon](https://www.amazon.com/gp/product/B0983R6MH2)~~ (the APK file repackaged by Amazon is incomplete! An issue report was never answered by Amazon.)
|
||||
|
||||
Please see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq173) about the differences between the different releases.
|
||||
Please see [this FAQ](https://m66b.github.io/FairEmail/#faq173) about the differences between the different releases.
|
||||
|
||||
**Important**: after enrolling in the [Advanced Protection Program](https://landing.google.com/advancedprotection/)
|
||||
you cannot use third party email apps anymore, please see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq22) for more information.
|
||||
you cannot use third party email apps anymore, please see [this FAQ](https://m66b.github.io/FairEmail/#faq22) for more information.
|
||||
|
||||
The Gmail quick setup wizard can be used in official releases only (Play store or GitHub) because Google approved the use of OAuth for one app signature only.
|
||||
|
||||
|
@ -143,7 +143,7 @@ F-Droid builds new versions irregularly and you'll need the F-Droid client to ge
|
|||
To get updates in a timely fashion you are advised to use the GitHub release.
|
||||
|
||||
**Important**: There is support on the F-Droid build only if the version number of the F-Droid build is the same as the version number of the latest GitHub release.
|
||||
Please [see here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq147) for more information on third-party builds.
|
||||
Please [see here](https://m66b.github.io/FairEmail/#faq147) for more information on third-party builds.
|
||||
|
||||
Because F-Droid builds and GitHub releases are signed differently, an F-Droid build needs to be uninstalled first to be able to update to a GitHub release.
|
||||
|
||||
|
@ -178,7 +178,7 @@ because earlier Android versions do not support notification channels.
|
|||
|
||||
FairEmail will work properly on devices without any Google service installed.
|
||||
|
||||
Please see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-known-problems) for known problems.
|
||||
Please see [here](https://m66b.github.io/FairEmail/#known-problems) for known problems.
|
||||
|
||||
## Privacy
|
||||
|
||||
|
@ -186,7 +186,7 @@ Please see [here](https://github.com/M66B/FairEmail/blob/master/PRIVACY.md#faire
|
|||
|
||||
## Support
|
||||
|
||||
Please see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md) for a list of often asked questions and about how to get support.
|
||||
Please see [here](https://m66b.github.io/FairEmail/) for a list of often asked questions and about how to get support.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -223,7 +223,7 @@ Error reporting is sponsored by:
|
|||
![Bugsnag logo](/images/bugsnag_logo.png)
|
||||
|
||||
[Bugsnag](https://www.bugsnag.com/) monitors application stability
|
||||
and is used to [help improve FairEmail](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104).
|
||||
and is used to [help improve FairEmail](https://m66b.github.io/FairEmail/#faq104).
|
||||
Error reporting is disabled by default, see also [the privacy policy](https://github.com/M66B/FairEmail/blob/master/PRIVACY.md#fairemail).
|
||||
|
||||
## License
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# https://developer.android.com/studio/projects/configure-cmake
|
||||
|
||||
project(fairemail)
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
add_library(fairemail SHARED
|
||||
src/main/jni/fairemail.cc
|
||||
|
|
|
@ -3,9 +3,9 @@ apply plugin: 'com.bugsnag.android.gradle'
|
|||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
def getVersionCode = { -> return 2176 }
|
||||
def getRevision = { -> return "a" }
|
||||
def getReleaseName = { -> return "Zby" }
|
||||
def getVersionCode = { -> return 2182 }
|
||||
def getRevision = { -> return "b" }
|
||||
def getReleaseName = { -> return "Acantholipan" }
|
||||
// https://en.wikipedia.org/wiki/List_of_dinosaur_genera
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
|
@ -18,6 +18,7 @@ if (rootProject.file("local.properties").exists())
|
|||
|
||||
android {
|
||||
//compileSdkExtension 4 // https://developer.android.com/guide/sdk-extensions
|
||||
//compileSdkPreview "VanillaIceCream"
|
||||
namespace 'eu.faircode.email'
|
||||
|
||||
// https://apilevels.com/
|
||||
|
@ -26,6 +27,7 @@ android {
|
|||
compileSdk 34
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
//targetSdkPreview "VanillaIceCream"
|
||||
versionCode getVersionCode()
|
||||
versionName "1." + getVersionCode()
|
||||
|
||||
|
@ -51,7 +53,7 @@ android {
|
|||
}
|
||||
|
||||
// https://developer.android.com/ndk/downloads
|
||||
ndkVersion "25.2.9519653" // r25c
|
||||
ndkVersion "26.3.11579264" // r26d
|
||||
ndk {
|
||||
// Bugsnag, sqlite
|
||||
// https://developer.android.com/ndk/guides/abis
|
||||
|
@ -116,7 +118,9 @@ android {
|
|||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true // https://issuetracker.google.com/issues/127691101
|
||||
// https://issuetracker.google.com/issues/127691101
|
||||
// https://developer.android.com/guide/practices/page-sizes#build
|
||||
useLegacyPackaging = true
|
||||
excludes += [
|
||||
'org/apache/**'
|
||||
]
|
||||
|
@ -153,15 +157,17 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// https://developer.android.com/reference/tools/gradle-api/8.3/com/android/build/api/dsl/BuildFeatures
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = false
|
||||
dataBinding = false
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
compose = false
|
||||
prefab = false
|
||||
renderScript = false
|
||||
resValues = false
|
||||
shaders = false
|
||||
compose = false
|
||||
viewBinding = false
|
||||
dataBinding = false
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -215,6 +221,8 @@ android {
|
|||
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
|
||||
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
|
||||
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
|
||||
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
large {
|
||||
|
@ -239,6 +247,8 @@ android {
|
|||
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
|
||||
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
|
||||
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
|
||||
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
fdroid {
|
||||
|
@ -272,6 +282,8 @@ android {
|
|||
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
|
||||
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
|
||||
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
|
||||
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
play {
|
||||
|
@ -293,10 +305,12 @@ android {
|
|||
buildConfigField "String", "ANNOUNCEMENT_URI", "\"\""
|
||||
buildConfigField "String", "CLOUD_URI", "\"\""
|
||||
buildConfigField "String", "CLOUD_EMAIL", "\"\""
|
||||
buildConfigField "String", "OPENAI_ENDPOINT", "\"\""
|
||||
buildConfigField "String", "OPENAI_PRIVACY", "\"\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"\""
|
||||
buildConfigField "String", "OPENAI_ENDPOINT", "\"https://api.openai.com/v1/\""
|
||||
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
|
||||
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
|
||||
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
|
||||
buildConfigField "String", "FDROID", "\"\""
|
||||
getIsDefault().set(true)
|
||||
}
|
||||
|
@ -323,6 +337,8 @@ android {
|
|||
buildConfigField "String", "OPENAI_PRIVACY", "\"\""
|
||||
buildConfigField "String", "GEMINI_ENDPOINT", "\"\""
|
||||
buildConfigField "String", "GEMINI_PRIVACY", "\"\""
|
||||
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
|
||||
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
|
||||
buildConfigField "String", "FDROID", "\"\""
|
||||
}
|
||||
}
|
||||
|
@ -461,11 +477,11 @@ tasks.register('upload') {
|
|||
doLast {
|
||||
println "\nhttps://bitbucket.org/M66B/fairemail-test/downloads/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target + "-release.apk\n"
|
||||
exec {
|
||||
workingDir "${buildDir}"
|
||||
workingDir "${rootDir}/app/build"
|
||||
commandLine 'curl',
|
||||
'-o', '/dev/null',
|
||||
'-X', 'POST', "https://M66B:" + localProperties.getProperty("bb.pwd", "") + "@api.bitbucket.org/2.0/repositories/M66B/fairemail-test/downloads",
|
||||
'--form', "files=@${buildDir}/outputs/apk/" + target.split('-')[0] + "/release/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target.split('-')[0] + "-release.apk;" +
|
||||
'--form', "files=@${rootDir}/app/build/outputs/apk/" + target.split('-')[0] + "/release/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target.split('-')[0] + "-release.apk;" +
|
||||
"filename=FairEmail-v1." + getVersionCode() + getRevision() + "-" + target + "-release.apk"
|
||||
}
|
||||
}
|
||||
|
@ -528,50 +544,50 @@ dependencies {
|
|||
def desugar_version = "2.0.4"
|
||||
def startup_version = "1.2.0-alpha02"
|
||||
def annotation_version_experimental = "1.4.1"
|
||||
def core_version = "1.13.0"
|
||||
def appcompat_version = "1.6.1" // 1.7.0-alpha03
|
||||
def core_version = "1.13.1" // 1.14.0-alpha01
|
||||
def appcompat_version = "1.6.1" // 1.7.0-rc01
|
||||
def emoji_version = "1.4.0" // 1.5.0-alpha01
|
||||
def flatbuffers_version = "2.0.0"
|
||||
def activity_version = " 1.9.0"
|
||||
def fragment_version = "1.6.2" // 1.7.0-rc02
|
||||
def windows_version = "1.2.0" // 1.3.0-beta01
|
||||
def webkit_version = "1.10.0" // 1.11.0-rc01
|
||||
def fragment_version = "1.6.2" // 1.7.1 // 1.8.0-beta01
|
||||
def windows_version = "1.2.0" // 1.3.0-rc01
|
||||
def webkit_version = "1.10.0" // 1.11.0
|
||||
def recyclerview_version = "1.3.2" // 1.4.0-alpha01
|
||||
def coordinatorlayout_version = "1.2.0" // 1.3.0-alpha02
|
||||
def constraintlayout_version = "2.1.4" // 2.2.0-alpha13
|
||||
def material_version = "1.11.0" // 1.11.0-alpha01
|
||||
def material_version = "1.11.0" // 1.12.0-rc01 / 1.13.0-alpha01
|
||||
def browser_version = "1.8.0"
|
||||
def lbm_version = "1.1.0"
|
||||
def swiperefresh_version = "1.2.0-alpha01"
|
||||
def documentfile_version = "1.1.0-alpha01"
|
||||
def lifecycle_version = "2.7.0" // 2.8.0-beta01
|
||||
def lifecycle_version = "2.7.0" // 2.8.0
|
||||
def lifecycle_extensions_version = "2.2.0"
|
||||
def room_version = "2.4.3" // 2.5.2/2.6.1
|
||||
def sqlite_version = "2.4.0"
|
||||
def room_version = "2.4.3" // 2.5.2/2.6.1/2.7.0-alpha02
|
||||
def sqlite_version = "2.4.0" // 2.5.0-alpha02
|
||||
def requery_version = "3.39.2"
|
||||
def paging_version = "2.1.2" // 3.3.0-beta01
|
||||
def paging_version = "2.1.2" // 3.3.0-rc01
|
||||
def preference_version = "1.2.1"
|
||||
def work_version = "2.9.0" // 2.10.0-alpha02
|
||||
def exif_version = "1.3.7"
|
||||
def biometric_version = "1.2.0-alpha05"
|
||||
def billingclient_version = "6.0.1" // 6.2.0
|
||||
def playservicesbase_version = "18.3.0";
|
||||
def playservicesbasement_version = "18.3.0";
|
||||
def transparency_version = "2.5.19"
|
||||
def javamail_version = "1.6.7"
|
||||
def jsoup_version = "1.17.2"
|
||||
def jsonpath_version = "2.9.0"
|
||||
def css_version = "0.9.30"
|
||||
def jax_version = "2.3.0-jaxb-1.0.6"
|
||||
def minidns_version = "1.0.4"
|
||||
def minidns_version = "1.0.5"
|
||||
def openpgp_version = "12.0"
|
||||
def badge_version = "1.1.22"
|
||||
def bugsnag_version = "6.1.0"
|
||||
def bugsnag_version = "6.4.0"
|
||||
def biweekly_version = "0.6.8"
|
||||
def vcard_version = "0.12.1"
|
||||
def relinker_version = "1.4.5"
|
||||
def markwon_version = "4.6.2"
|
||||
def commonmark_version = "0.21.0"
|
||||
def bouncycastle_version = "1.77"
|
||||
def commonmark_version = "0.22.0"
|
||||
def bouncycastle_version = "1.78.1"
|
||||
def colorpicker_version = "0.0.15"
|
||||
def overscroll_version = "1.1.1"
|
||||
def appauth_version = "0.11.1"
|
||||
|
@ -581,9 +597,9 @@ dependencies {
|
|||
def reactivestreams_version = "1.0.3"
|
||||
def rxjava2_version = "2.2.21"
|
||||
def svg_version = "1.4"
|
||||
def compress_version = "1.25.0"
|
||||
def ipaddress_version = "5.4.0"
|
||||
def canary_version = "2.13"
|
||||
def compress_version = "1.26.1"
|
||||
def ipaddress_version = "5.5.0"
|
||||
def canary_version = "2.14"
|
||||
def ws_version = "2.14"
|
||||
def tinylog_version = "2.6.2"
|
||||
def zxing_version = "3.5.3"
|
||||
|
@ -688,7 +704,8 @@ dependencies {
|
|||
// https://mvnrepository.com/artifact/androidx.work/work-runtime
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
// implementation "com.google.guava:listenablefuture:1.0"
|
||||
implementation "com.google.guava:guava:31.1-android" // ListenableFuture
|
||||
// https://mvnrepository.com/artifact/com.google.guava/guava
|
||||
implementation "com.google.guava:guava:33.1.0-android" // ListenableFuture
|
||||
|
||||
// https://mvnrepository.com/artifact/androidx.exifinterface/exifinterface
|
||||
implementation "androidx.exifinterface:exifinterface:$exif_version"
|
||||
|
@ -701,10 +718,10 @@ dependencies {
|
|||
// https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.html
|
||||
playImplementation "com.android.billingclient:billing:$billingclient_version"
|
||||
|
||||
// https://mvnrepository.com/artifact/com.google.android.gms/play-services-base
|
||||
githubImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
|
||||
largeImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
|
||||
playImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
|
||||
// https://mvnrepository.com/artifact/com.google.android.gms/play-services-basement
|
||||
githubImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
|
||||
largeImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
|
||||
playImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
|
||||
|
||||
// https://github.com/appmattus/certificatetransparency
|
||||
// https://mvnrepository.com/artifact/com.appmattus.certificatetransparency/certificatetransparency-android
|
||||
|
@ -789,6 +806,7 @@ dependencies {
|
|||
implementation "org.commonmark:commonmark-ext-gfm-strikethrough:$commonmark_version";
|
||||
|
||||
// https://github.com/vsch/flexmark-java
|
||||
// https://mvnrepository.com/artifact/com.vladsch.flexmark/flexmark
|
||||
implementation "com.vladsch.flexmark:flexmark-html2md-converter:0.64.8"
|
||||
|
||||
// // https://github.com/QuadFlask/colorpicker
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -80,9 +80,12 @@ public class ActivityBilling extends ActivityBase implements PurchasingListener,
|
|||
if (standalone) {
|
||||
setContentView(R.layout.activity_billing);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
int count = getSupportFragmentManager().getBackStackEntryCount();
|
||||
if (count == 0) {
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
|
|
|
@ -366,6 +366,9 @@ public class Bimi {
|
|||
}
|
||||
}
|
||||
|
||||
if (bitmap != null && !verified)
|
||||
Log.i("BIMI unverified");
|
||||
|
||||
return (bitmap == null ? null : new Pair<>(bitmap, verified));
|
||||
}
|
||||
|
||||
|
|
|
@ -98,9 +98,12 @@ public class ActivityBilling extends ActivityBase implements
|
|||
if (standalone) {
|
||||
setContentView(R.layout.activity_billing);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
int count = getSupportFragmentManager().getBackStackEntryCount();
|
||||
if (count == 0) {
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
|
|
|
@ -98,9 +98,12 @@ public class ActivityBilling extends ActivityBase implements
|
|||
if (standalone) {
|
||||
setContentView(R.layout.activity_billing);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
int count = getSupportFragmentManager().getBackStackEntryCount();
|
||||
if (count == 0) {
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
|
@ -314,96 +317,97 @@ public class ActivityBilling extends ActivityBase implements
|
|||
Log.unexpectedError(getSupportFragmentManager(), ex);
|
||||
}
|
||||
}
|
||||
/*
|
||||
private void onPurchaseConsume(Intent intent) {
|
||||
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, new PurchasesResponseListener() {
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
for (Purchase purchase : list)
|
||||
consumePurchase(purchase);
|
||||
} else
|
||||
reportError(result, "IAB onPurchaseConsume");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onPurchaseError(Intent intent) {
|
||||
String message = intent.getStringExtra("message");
|
||||
boolean play = Helper.hasPlayStore(this);
|
||||
Uri uri = Helper.getSupportUri(this, "Purchase:error");
|
||||
if (!TextUtils.isEmpty(message))
|
||||
uri = uri
|
||||
.buildUpon()
|
||||
.appendQueryParameter("message", "IAB: " + message + " Play: " + play)
|
||||
.build();
|
||||
Helper.view(this, uri, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult result) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
EntityLog.log(this, "IAB connected");
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onConnected();
|
||||
|
||||
backoff = 4;
|
||||
queryPurchases();
|
||||
} else
|
||||
reportError(result, "IAB connected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
EntityLog.log(this, "IAB disconnected");
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onDisconnected();
|
||||
|
||||
backoff *= 2;
|
||||
retry(backoff);
|
||||
}
|
||||
|
||||
private void retry(int backoff) {
|
||||
Log.i("IAB connect retry in " + backoff + " s");
|
||||
|
||||
getMainHandler().postDelayed(new RunnableEx("IAB retry") {
|
||||
@Override
|
||||
public void delegate() {
|
||||
try {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
boolean ready = billingClient.isReady();
|
||||
Log.i("IAB ready=" + ready);
|
||||
if (!ready)
|
||||
billingClient.startConnection(ActivityBilling.this);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
/*
|
||||
private void onPurchaseConsume(Intent intent) {
|
||||
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, new PurchasesResponseListener() {
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
for (Purchase purchase : list)
|
||||
consumePurchase(purchase);
|
||||
} else
|
||||
reportError(result, "IAB onPurchaseConsume");
|
||||
}
|
||||
}
|
||||
}, backoff * 1000L);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult result, @Nullable List<Purchase> purchases) {
|
||||
Log.i("IAB purchases updated");
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
|
||||
checkPurchases(purchases);
|
||||
else
|
||||
reportError(result, "IAB purchases updated");
|
||||
}
|
||||
private void onPurchaseError(Intent intent) {
|
||||
String message = intent.getStringExtra("message");
|
||||
boolean play = Helper.hasPlayStore(this);
|
||||
Uri uri = Helper.getSupportUri(this, "Purchase:error");
|
||||
if (!TextUtils.isEmpty(message))
|
||||
uri = uri
|
||||
.buildUpon()
|
||||
.appendQueryParameter("message", "IAB: " + message + " Play: " + play)
|
||||
.build();
|
||||
Helper.view(this, uri, true);
|
||||
}
|
||||
|
||||
private void queryPurchases() {
|
||||
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, this);
|
||||
}
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult result) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
EntityLog.log(this, "IAB connected");
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onConnected();
|
||||
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
|
||||
checkPurchases(list);
|
||||
else
|
||||
reportError(result, "IAB query purchases");
|
||||
}
|
||||
*/
|
||||
backoff = 4;
|
||||
queryPurchases();
|
||||
} else
|
||||
reportError(result, "IAB connected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
EntityLog.log(this, "IAB disconnected");
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onDisconnected();
|
||||
|
||||
backoff *= 2;
|
||||
retry(backoff);
|
||||
}
|
||||
|
||||
private void retry(int backoff) {
|
||||
Log.i("IAB connect retry in " + backoff + " s");
|
||||
|
||||
getMainHandler().postDelayed(new RunnableEx("IAB retry") {
|
||||
@Override
|
||||
public void delegate() {
|
||||
try {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
boolean ready = billingClient.isReady();
|
||||
Log.i("IAB ready=" + ready);
|
||||
if (!ready)
|
||||
billingClient.startConnection(ActivityBilling.this);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
}, backoff * 1000L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult result, @Nullable List<Purchase> purchases) {
|
||||
Log.i("IAB purchases updated");
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
|
||||
checkPurchases(purchases);
|
||||
else
|
||||
reportError(result, "IAB purchases updated");
|
||||
}
|
||||
|
||||
private void queryPurchases() {
|
||||
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
|
||||
checkPurchases(list);
|
||||
else
|
||||
reportError(result, "IAB query purchases");
|
||||
}
|
||||
*/
|
||||
interface IBillingListener {
|
||||
void onConnected();
|
||||
|
||||
|
|
|
@ -4,9 +4,80 @@
|
|||
|
||||
For support you can use [the contact form](https://contact.faircode.eu/?product=fairemailsupport).
|
||||
|
||||
### [Acantholipan](https://en.wikipedia.org/wiki/Acantholipan)
|
||||
|
||||
### Next version
|
||||
|
||||
* Prepared for Android 15
|
||||
* Added "AI" summarize rule action
|
||||
* Listing NOT rule conditions
|
||||
* Reverted AndroidX fragment to version 1.6.2
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
Preview versions are available [here](https://bitbucket.org/M66B/fairemail-test/downloads/).
|
||||
|
||||
### 1.2182 - 2024-05-15
|
||||
|
||||
* Added optional "AI" summarize quick action
|
||||
* Added optional "AI" summarize swipe action
|
||||
* Changed default OpenAI model to [gpt-4o](https://openai.com/index/hello-gpt-4o/)
|
||||
* Improved OpenAI integration (added multimodal support)
|
||||
* Improved Gemini integration
|
||||
* Made "AI" integrations available in the Play Store version
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
*The use of "AI" integrations is and will remain completely optional*
|
||||
|
||||
### 1.2181 - 2024-05-13
|
||||
|
||||
* Reverted [AndroidX ROOM](https://developer.android.com/jetpack/androidx/releases/room#2.6.1) to version 2.4.3 to solve locking issues (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2180 - 2024-05-13
|
||||
|
||||
* Improved [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
|
||||
* Performance improvements
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [NDK](https://developer.android.com/ndk/)
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2179 - 2024-05-08
|
||||
|
||||
* Added option to change "AI" summarize prompt
|
||||
* Added expression condition functions, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated build tools
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated libraries (including [Bouncy Castle](https://www.bouncycastle.org/))
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2178 - 2024-04-29
|
||||
|
||||
* Added "AI" summarization of received messages (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sup>(*) Via the horizontal three-dots button just above the message text. ChatGPT or Gemini needs to be configured in the integrations-settings tab page for this.</sub>
|
||||
|
||||
### 1.2177 - 2024-04-27
|
||||
|
||||
* Added [Have I Been Pwned?](https://haveibeenpwned.com/) **<ins>password</ins>** check (*)
|
||||
* Added identity option to configure envelope-from (*MAIL FROM*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sub>(*) Via the three-dots overflow menu of the account list under "*Manual setup and account options*" in the main settings screen (GitHub version only)</sub>
|
||||
|
||||
### [Zby](https://en.wikipedia.org/wiki/Zby)
|
||||
|
||||
### 1.2176 - 2024-04-22
|
||||
### 1.2176 - 2024-04-22 *
|
||||
|
||||
* Fixed British English translation
|
||||
* Small improvements and minor bug fixes
|
||||
|
@ -39,7 +110,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
(*) Currently supported email forwarders:
|
||||
<sub>(*) Currently supported email forwarders:</sub>
|
||||
|
||||
* [addy.io](https://addy.io/)
|
||||
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
|
||||
|
@ -231,6 +302,8 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Updated libraries (Apache Compress, Bugsnag, Bouncy Castle, Jsoup)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<!-- truncate here -->
|
||||
|
||||
### 1.2145 - 2023-12-30
|
||||
|
||||
* Added Adguard filter list to remove tracking parameters from links, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq200)
|
||||
|
@ -1641,7 +1714,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
* Small improvements and minor bug fixes
|
||||
* Updated translations
|
||||
|
||||
(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required
|
||||
<sub>(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required</sub>
|
||||
|
||||
### 1.1930 - 2022-07-04
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Einrichtungshilfe
|
||||
|
||||
Es ist ziemlich einfach, FairEmail einzurichten. Sie müssen mindestens ein Konto hinzufügen, um E-Mails zu empfangen, und mindestens eine Identität, um E-Mails zu senden. Die Schnelleinrichtung wird ein Konto und eine Identität in einem Schritt für die meisten großen Anbieter hinzufügen.
|
||||
Die Einrichtung von FairEmail ist ziemlich einfach. Sie müssen mindestens ein Konto hinzufügen, um E-Mails zu empfangen, und mindestens eine Identität, um E-Mails zu senden. Die Schnelleinrichtung wird ein Konto und eine Identität in einem Schritt für die meisten großen Anbieter hinzufügen.
|
||||
|
||||
## Anforderungen
|
||||
|
||||
|
@ -10,7 +10,7 @@ Für die Einrichtung von Konten und Identitäten ist eine Internetverbindung erf
|
|||
|
||||
Wählen Sie einfach den passenden Anbieter oder *»Anderer Anbieter«* aus, geben Ihren Namen, Ihre E-Mail-Adresse und Ihr Passwort ein und tippen auf *»Überprüfen«*.
|
||||
|
||||
Dies funktioniert für die meisten E-Mail-Anbieter.
|
||||
Das funktioniert für die meisten großen E-Mail-Anbieter.
|
||||
|
||||
Wenn die Schnelleinrichtung nicht funktioniert, müssen Sie Konto und Identität manuell einrichten, siehe Anweisungen unten.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Guida di configurazione
|
||||
|
||||
Configurare FairEmail è abbastanza semplice. Dovrai aggiungere almeno un profilo per ricevere le email e almeno un'identità se vuoi inviarle. La configurazione rapida aggiungerà un profilo e un'identità in una sola volta per gran parte dei principali fornitori.
|
||||
Configurare FairEmail è semplice. Aggiungi almeno un profilo (account di posta) per ricevere le email e almeno un'identità se vuoi inviarle. La configurazione rapida aggiungerà un profilo e un'identità in modalità guidata (valida per i principali provider di posta).
|
||||
|
||||
## Requisiti
|
||||
|
||||
|
@ -8,34 +8,34 @@ Configurare FairEmail è abbastanza semplice. Dovrai aggiungere almeno un profil
|
|||
|
||||
## Configurazione rapida
|
||||
|
||||
Basta selezionare il provider appropriato o *Altri provider* e inserire il tuo nome, l'indirizzo email e la password, e toccare *Controlla*.
|
||||
Basta selezionare il provider appropriato o *Altri provider* e inserire il tuo nome, l'indirizzo email e la password, e toccare *Controlla*:
|
||||
|
||||
Questo funzionerà per gran parte dei provider email.
|
||||
non sarà necessario configurare altro.
|
||||
|
||||
Se la configurazione rapida non funziona, dovrai configurare manualmente un profilo e un'identità, vedi sotto per le istruzioni.
|
||||
Se la configurazione rapida non funziona, dovrai impostare manualmente un profilo e un'identità: leggi le istruzioni qui sotto.
|
||||
|
||||
## Configura il profilo - per ricevere le email
|
||||
|
||||
Per aggiungere un profilo, tocca *Configurazione manuale e altre opzioni*, tocca *Profili* e il pulsante 'più' in fondo e seleziona IMAP (o POP3). Seleziona un provider dall'elenco, inserisci il nome utente, che è prevalentemente il tuo indirizzo email e inserisci la tua password. Tocca *Controlla* per far connettere FairEmail al server email e recuperare un elenco delle cartelle di sistema. Dopo aver revisionato la selezione delle cartelle di sistema, puoi aggiungere il profilo toccando *Salva*.
|
||||
Per aggiungere un profilo, tocca *Configurazione manuale e altre opzioni*, tocca *Profili* e il pulsante 'più' in fondo e seleziona IMAP (o POP3). Seleziona un provider dall'elenco, inserisci il nome utente, che è quasi sempre il tuo indirizzo email, e la tua password. Tocca *Controlla* per collegare FairEmail al server e recuperare l'elenco delle cartelle di posta. Controlla le cartelle selezionate, poi conferma l'aggiunta dell'account con il tasto *Salva*.
|
||||
|
||||
Se il tuo provider non è nell'elenco, ce ne sono a migliaia, seleziona *Personalizzato*. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Ottieni le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta, altrimenti controlla le istruzioni di configurazione del tuo provider per il giusto nome dell'host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS). Per altro a riguardo, sei pregato di vedere [qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md#authorizing-accounts).
|
||||
I provider di posta sono migliaia: se il tuo non è nell'elenco, seleziona *Personalizzato*. Inserisci il nome del dominio (es. *gmail.com*) e tocca *Scarica le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta; altrimenti cerca le impostazioni più recenti sul sito del tuo provider. I parametri obbligatori sono: host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS). Clicca [qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md#authorizing-accounts) se ti occorrono altre informazioni.
|
||||
|
||||
## Configura l'identità - per inviare le email
|
||||
|
||||
Similmente, per aggiungere un'identità, tocca *Configurazione manuale e altre opzioni*, tocca *Identità* e il pulsante 'più' in fondo. Inserisci il nome che vuoi compaia nell'indirizzo del mittente delle email che invii e seleziona un profilo collegato. Tocca *Salva* per aggiungere l'identità.
|
||||
Per aggiungere un'identità, tocca *Configurazione manuale e altre opzioni*, poi *Identità* e infine il pulsante + ('più') in fondo. Inserisci il tuo nome (quello che apparirà ai destinatari come mittente), poi scegli un account a cui collegarlo. Tocca *Salva* per aggiungere l'identità.
|
||||
|
||||
Se il profilo è stato configurato manualmente, potresti dover configurare manualmente anche l'identità. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Ottieni le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta, altrimenti controlla le istruzioni di configurazione del tuo provider per il giusto nome dell'host SMTP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS).
|
||||
Se il profilo è stato configurato manualmente, potresti dover configurare manualmente anche l'identità. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Scarica le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta; altrimenti cerca le impostazioni più recenti sul sito del tuo provider. I parametri obbligatori sono: host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS).
|
||||
|
||||
Vedi [questa FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#FAQ9) sull'uso degli alias.
|
||||
Per l'utilizzo degli alias, vedi [questa FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#FAQ9).
|
||||
|
||||
## Concedi le autorizzazioni - per accedere alle informazioni di contatto
|
||||
## Configura l'accesso ai contatti (Rubrica di sistema)
|
||||
|
||||
Se vuoi cercare gli indirizzi email, visualizzare le foto di contatto, etc., dovrai concedere le autorizzazioni per leggere le informazioni di contatto a FairEmail. Basta toccare su *Autorizza* e selezionare *Consenti*.
|
||||
Se vuoi cercare gli indirizzi email dei tuoi contatti Google, visualizzare le foto e rendere più semplice l'interazione con la rubrica di sistema, dovrai autorizzare FairEmail a leggere le informazioni di contatto. Basta toccare *Autorizza* e di seguito *Consenti*.
|
||||
|
||||
## Configura le ottimizzazioni della batteria - per ricevere costantemente le email
|
||||
## Consentire a FairEmail di ricevere le email in tempo reale
|
||||
|
||||
Sulle versioni recenti di Android, Android metterà in standby le app quando lo schermo è spento per un po' di tempo per ridurre l'uso della batteria. Se vuoi ricevere le nuove email senza ritardo, dovresti disabilitare le ottimizzazioni della batteria per FairEmail. Tocca *Gestisci* e segui le istruzioni.
|
||||
Per limitare al massimo l'uso della batteria, Android chiude automaticamente le app in background dopo un certo numero di minuti. Le uniche app che possono funzionare in background sono quelle di sistema e quelle a cui l'utente concede le autorizzazioni necessarie. Tocca *Gestisci* e segui le istruzioni.
|
||||
|
||||
## Domande o problemi
|
||||
## Altro
|
||||
|
||||
Se hai una domanda o un problema, sei pregato di [vedere qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md) per aiuto.
|
||||
Se hai una domanda o un problema, sei pregato di [controllare qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md) per trovare una risposta o chiedere assistenza.
|
|
@ -6710,7 +6710,7 @@ org.zw
|
|||
|
||||
// newGTLDs
|
||||
|
||||
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2024-03-28T15:13:37Z
|
||||
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2024-05-04T15:12:50Z
|
||||
// This list is auto-generated, don't edit it manually.
|
||||
// aaa : American Automobile Association, Inc.
|
||||
// https://www.iana.org/domains/root/db/aaa.html
|
||||
|
@ -6896,7 +6896,7 @@ anquan
|
|||
// https://www.iana.org/domains/root/db/anz.html
|
||||
anz
|
||||
|
||||
// aol : Oath Inc.
|
||||
// aol : Yahoo Inc.
|
||||
// https://www.iana.org/domains/root/db/aol.html
|
||||
aol
|
||||
|
||||
|
@ -11120,7 +11120,7 @@ xyz
|
|||
// https://www.iana.org/domains/root/db/yachts.html
|
||||
yachts
|
||||
|
||||
// yahoo : Oath Inc.
|
||||
// yahoo : Yahoo Inc.
|
||||
// https://www.iana.org/domains/root/db/yahoo.html
|
||||
yahoo
|
||||
|
||||
|
@ -11200,6 +11200,10 @@ ltd.ua
|
|||
// 611coin : https://611project.org/
|
||||
611.to
|
||||
|
||||
// AAA workspace : https://aaa.vodka
|
||||
// Submitted by Kirill Rezraf <admin@aaa.vodka>
|
||||
aaa.vodka
|
||||
|
||||
// A2 Hosting
|
||||
// Submitted by Tyler Hall <sysadmin@a2hosting.com>
|
||||
a2hosted.com
|
||||
|
@ -11346,23 +11350,28 @@ cloudfront.net
|
|||
|
||||
// Amazon Cognito
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: 7bee1013-f456-47df-bfe8-03c78d946d61
|
||||
// Reference: 09588633-91fe-49d8-b4e7-ec36496d11f3
|
||||
auth.af-south-1.amazoncognito.com
|
||||
auth.ap-northeast-1.amazoncognito.com
|
||||
auth.ap-northeast-2.amazoncognito.com
|
||||
auth.ap-northeast-3.amazoncognito.com
|
||||
auth.ap-south-1.amazoncognito.com
|
||||
auth.ap-south-2.amazoncognito.com
|
||||
auth.ap-southeast-1.amazoncognito.com
|
||||
auth.ap-southeast-2.amazoncognito.com
|
||||
auth.ap-southeast-3.amazoncognito.com
|
||||
auth.ap-southeast-4.amazoncognito.com
|
||||
auth.ca-central-1.amazoncognito.com
|
||||
auth.eu-central-1.amazoncognito.com
|
||||
auth.eu-central-2.amazoncognito.com
|
||||
auth.eu-north-1.amazoncognito.com
|
||||
auth.eu-south-1.amazoncognito.com
|
||||
auth.eu-south-2.amazoncognito.com
|
||||
auth.eu-west-1.amazoncognito.com
|
||||
auth.eu-west-2.amazoncognito.com
|
||||
auth.eu-west-3.amazoncognito.com
|
||||
auth.il-central-1.amazoncognito.com
|
||||
auth.me-central-1.amazoncognito.com
|
||||
auth.me-south-1.amazoncognito.com
|
||||
auth.sa-east-1.amazoncognito.com
|
||||
auth.us-east-1.amazoncognito.com
|
||||
|
@ -11385,7 +11394,7 @@ us-east-1.amazonaws.com
|
|||
|
||||
// Amazon EMR
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: 597f3f8e-9283-4e48-8e32-7ee25a1ff6ab
|
||||
// Reference: 82f43f9f-bbb8-400e-8349-854f5a62f20d
|
||||
emrappui-prod.cn-north-1.amazonaws.com.cn
|
||||
emrnotebooks-prod.cn-north-1.amazonaws.com.cn
|
||||
emrstudio-prod.cn-north-1.amazonaws.com.cn
|
||||
|
@ -11410,6 +11419,9 @@ emrstudio-prod.ap-northeast-3.amazonaws.com
|
|||
emrappui-prod.ap-south-1.amazonaws.com
|
||||
emrnotebooks-prod.ap-south-1.amazonaws.com
|
||||
emrstudio-prod.ap-south-1.amazonaws.com
|
||||
emrappui-prod.ap-south-2.amazonaws.com
|
||||
emrnotebooks-prod.ap-south-2.amazonaws.com
|
||||
emrstudio-prod.ap-south-2.amazonaws.com
|
||||
emrappui-prod.ap-southeast-1.amazonaws.com
|
||||
emrnotebooks-prod.ap-southeast-1.amazonaws.com
|
||||
emrstudio-prod.ap-southeast-1.amazonaws.com
|
||||
|
@ -11419,18 +11431,30 @@ emrstudio-prod.ap-southeast-2.amazonaws.com
|
|||
emrappui-prod.ap-southeast-3.amazonaws.com
|
||||
emrnotebooks-prod.ap-southeast-3.amazonaws.com
|
||||
emrstudio-prod.ap-southeast-3.amazonaws.com
|
||||
emrappui-prod.ap-southeast-4.amazonaws.com
|
||||
emrnotebooks-prod.ap-southeast-4.amazonaws.com
|
||||
emrstudio-prod.ap-southeast-4.amazonaws.com
|
||||
emrappui-prod.ca-central-1.amazonaws.com
|
||||
emrnotebooks-prod.ca-central-1.amazonaws.com
|
||||
emrstudio-prod.ca-central-1.amazonaws.com
|
||||
emrappui-prod.ca-west-1.amazonaws.com
|
||||
emrnotebooks-prod.ca-west-1.amazonaws.com
|
||||
emrstudio-prod.ca-west-1.amazonaws.com
|
||||
emrappui-prod.eu-central-1.amazonaws.com
|
||||
emrnotebooks-prod.eu-central-1.amazonaws.com
|
||||
emrstudio-prod.eu-central-1.amazonaws.com
|
||||
emrappui-prod.eu-central-2.amazonaws.com
|
||||
emrnotebooks-prod.eu-central-2.amazonaws.com
|
||||
emrstudio-prod.eu-central-2.amazonaws.com
|
||||
emrappui-prod.eu-north-1.amazonaws.com
|
||||
emrnotebooks-prod.eu-north-1.amazonaws.com
|
||||
emrstudio-prod.eu-north-1.amazonaws.com
|
||||
emrappui-prod.eu-south-1.amazonaws.com
|
||||
emrnotebooks-prod.eu-south-1.amazonaws.com
|
||||
emrstudio-prod.eu-south-1.amazonaws.com
|
||||
emrappui-prod.eu-south-2.amazonaws.com
|
||||
emrnotebooks-prod.eu-south-2.amazonaws.com
|
||||
emrstudio-prod.eu-south-2.amazonaws.com
|
||||
emrappui-prod.eu-west-1.amazonaws.com
|
||||
emrnotebooks-prod.eu-west-1.amazonaws.com
|
||||
emrstudio-prod.eu-west-1.amazonaws.com
|
||||
|
@ -11440,6 +11464,9 @@ emrstudio-prod.eu-west-2.amazonaws.com
|
|||
emrappui-prod.eu-west-3.amazonaws.com
|
||||
emrnotebooks-prod.eu-west-3.amazonaws.com
|
||||
emrstudio-prod.eu-west-3.amazonaws.com
|
||||
emrappui-prod.il-central-1.amazonaws.com
|
||||
emrnotebooks-prod.il-central-1.amazonaws.com
|
||||
emrstudio-prod.il-central-1.amazonaws.com
|
||||
emrappui-prod.me-central-1.amazonaws.com
|
||||
emrnotebooks-prod.me-central-1.amazonaws.com
|
||||
emrstudio-prod.me-central-1.amazonaws.com
|
||||
|
@ -11470,9 +11497,11 @@ emrstudio-prod.us-west-2.amazonaws.com
|
|||
|
||||
// Amazon Managed Workflows for Apache Airflow
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: 4ab55e6f-90c0-4a8d-b6a0-52ca5dbb1c2e
|
||||
// Reference: 87f24ece-a77e-40e8-bb4a-f6b74fe9f975
|
||||
*.cn-north-1.airflow.amazonaws.com.cn
|
||||
*.cn-northwest-1.airflow.amazonaws.com.cn
|
||||
*.af-south-1.airflow.amazonaws.com
|
||||
*.ap-east-1.airflow.amazonaws.com
|
||||
*.ap-northeast-1.airflow.amazonaws.com
|
||||
*.ap-northeast-2.airflow.amazonaws.com
|
||||
*.ap-south-1.airflow.amazonaws.com
|
||||
|
@ -11481,12 +11510,15 @@ emrstudio-prod.us-west-2.amazonaws.com
|
|||
*.ca-central-1.airflow.amazonaws.com
|
||||
*.eu-central-1.airflow.amazonaws.com
|
||||
*.eu-north-1.airflow.amazonaws.com
|
||||
*.eu-south-1.airflow.amazonaws.com
|
||||
*.eu-west-1.airflow.amazonaws.com
|
||||
*.eu-west-2.airflow.amazonaws.com
|
||||
*.eu-west-3.airflow.amazonaws.com
|
||||
*.me-south-1.airflow.amazonaws.com
|
||||
*.sa-east-1.airflow.amazonaws.com
|
||||
*.us-east-1.airflow.amazonaws.com
|
||||
*.us-east-2.airflow.amazonaws.com
|
||||
*.us-west-1.airflow.amazonaws.com
|
||||
*.us-west-2.airflow.amazonaws.com
|
||||
|
||||
// Amazon S3
|
||||
|
@ -11780,9 +11812,25 @@ s3-fips.us-west-2.amazonaws.com
|
|||
s3-object-lambda.us-west-2.amazonaws.com
|
||||
s3-website.us-west-2.amazonaws.com
|
||||
|
||||
// Amazon SageMaker Ground Truth
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: 98dbfde4-7802-48c3-8751-b60f204e0d9c
|
||||
labeling.ap-northeast-1.sagemaker.aws
|
||||
labeling.ap-northeast-2.sagemaker.aws
|
||||
labeling.ap-south-1.sagemaker.aws
|
||||
labeling.ap-southeast-1.sagemaker.aws
|
||||
labeling.ap-southeast-2.sagemaker.aws
|
||||
labeling.ca-central-1.sagemaker.aws
|
||||
labeling.eu-central-1.sagemaker.aws
|
||||
labeling.eu-west-1.sagemaker.aws
|
||||
labeling.eu-west-2.sagemaker.aws
|
||||
labeling.us-east-1.sagemaker.aws
|
||||
labeling.us-east-2.sagemaker.aws
|
||||
labeling.us-west-2.sagemaker.aws
|
||||
|
||||
// Amazon SageMaker Notebook Instances
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: ce8ae0b1-0070-496d-be88-37c31837af9d
|
||||
// Reference: b5ea56df-669e-43cc-9537-14aa172f5dfc
|
||||
notebook.af-south-1.sagemaker.aws
|
||||
notebook.ap-east-1.sagemaker.aws
|
||||
notebook.ap-northeast-1.sagemaker.aws
|
||||
|
@ -11819,6 +11867,7 @@ notebook-fips.us-gov-east-1.sagemaker.aws
|
|||
notebook.us-gov-west-1.sagemaker.aws
|
||||
notebook-fips.us-gov-west-1.sagemaker.aws
|
||||
notebook.us-west-1.sagemaker.aws
|
||||
notebook-fips.us-west-1.sagemaker.aws
|
||||
notebook.us-west-2.sagemaker.aws
|
||||
notebook-fips.us-west-2.sagemaker.aws
|
||||
notebook.cn-north-1.sagemaker.com.cn
|
||||
|
@ -11826,7 +11875,7 @@ notebook.cn-northwest-1.sagemaker.com.cn
|
|||
|
||||
// Amazon SageMaker Studio
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: 057ee397-6bf8-4f20-b807-d7bc145ac980
|
||||
// Reference: 69c723d9-6e1a-4bff-a203-48eecd203183
|
||||
studio.af-south-1.sagemaker.aws
|
||||
studio.ap-east-1.sagemaker.aws
|
||||
studio.ap-northeast-1.sagemaker.aws
|
||||
|
@ -11840,6 +11889,7 @@ studio.ca-central-1.sagemaker.aws
|
|||
studio.eu-central-1.sagemaker.aws
|
||||
studio.eu-north-1.sagemaker.aws
|
||||
studio.eu-south-1.sagemaker.aws
|
||||
studio.eu-south-2.sagemaker.aws
|
||||
studio.eu-west-1.sagemaker.aws
|
||||
studio.eu-west-2.sagemaker.aws
|
||||
studio.eu-west-3.sagemaker.aws
|
||||
|
@ -11951,6 +12001,11 @@ webview-assets.aws-cloud9.us-west-2.amazonaws.com
|
|||
vfs.cloud9.us-west-2.amazonaws.com
|
||||
webview-assets.cloud9.us-west-2.amazonaws.com
|
||||
|
||||
// AWS Directory Service
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: a13203e8-42dc-4045-a0d2-2ee67bed1068
|
||||
awsapps.com
|
||||
|
||||
// AWS Elastic Beanstalk
|
||||
// Submitted by AWS Security <psl-maintainers@amazon.com>
|
||||
// Reference: bb5a965c-dec3-4967-aa22-e306ad064797
|
||||
|
@ -12144,6 +12199,10 @@ pages.gay
|
|||
// Submitted by Adrian <adrian@betainabox.com>
|
||||
betainabox.com
|
||||
|
||||
// University of Bielsko-Biala regional domain: http://dns.bielsko.pl/
|
||||
// Submitted by Marcin <dns@ath.bielsko.pl>
|
||||
bielsko.pl
|
||||
|
||||
// BinaryLane : http://www.binarylane.com
|
||||
// Submitted by Nathan O'Sullivan <nathan@mammoth.com.au>
|
||||
bnr.la
|
||||
|
@ -12190,7 +12249,8 @@ square7.net
|
|||
*.s.brave.io
|
||||
|
||||
// Brendly : https://brendly.rs
|
||||
// Submitted by Dusan Radovanovic <dusan.radovanovic@brendly.rs>
|
||||
// Submitted by Dusan Radovanovic <administracija@brendly.rs>
|
||||
shop.brendly.hr
|
||||
shop.brendly.rs
|
||||
|
||||
// BrowserSafetyMark
|
||||
|
@ -12307,7 +12367,10 @@ discourse.team
|
|||
|
||||
// Clever Cloud : https://www.clever-cloud.com/
|
||||
// Submitted by Quentin Adam <noc@clever-cloud.com>
|
||||
cleverapps.cc
|
||||
*.services.clever-cloud.com
|
||||
cleverapps.io
|
||||
cleverapps.tech
|
||||
|
||||
// Clerk : https://www.clerk.dev
|
||||
// Submitted by Colin Sidoti <systems@clerk.dev>
|
||||
|
@ -13093,6 +13156,11 @@ us-2.evennode.com
|
|||
us-3.evennode.com
|
||||
us-4.evennode.com
|
||||
|
||||
// Expo : https://expo.dev/
|
||||
// Submitted by James Ide <psl@expo.dev>
|
||||
expo.app
|
||||
staging.expo.app
|
||||
|
||||
// eDirect Corp. : https://hosting.url.com.tw/
|
||||
// Submitted by C.S. chang <cschang@corp.url.com.tw>
|
||||
twmail.cc
|
||||
|
@ -13391,10 +13459,6 @@ localcert.net
|
|||
localhostcert.net
|
||||
corpnet.work
|
||||
|
||||
// Ghost Foundation : https://ghost.org
|
||||
// Submitted by Matt Hanley <security@ghost.org>
|
||||
ghost.io
|
||||
|
||||
// GignoSystemJapan: http://gsj.bz
|
||||
// Submitted by GignoSystemJapan <kakutou-ec@gsj.bz>
|
||||
gsj.bz
|
||||
|
@ -13559,7 +13623,8 @@ ro.im
|
|||
goip.de
|
||||
|
||||
// Google, Inc.
|
||||
// Submitted by Eduardo Vela <evn@google.com>
|
||||
// Submitted by Shannon McCabe <public-suffix-editors@google.com>
|
||||
*.hosted.app
|
||||
*.run.app
|
||||
web.app
|
||||
*.0emm.com
|
||||
|
@ -14063,10 +14128,6 @@ co.network
|
|||
co.place
|
||||
co.technology
|
||||
|
||||
// Lightmaker Property Manager, Inc. : https://app.lmpm.com/
|
||||
// Submitted by Greg Holland <greg.holland@lmpm.com>
|
||||
app.lmpm.com
|
||||
|
||||
// linkyard ldt: https://www.linkyard.ch/
|
||||
// Submitted by Mario Siegenthaler <mario.siegenthaler@linkyard.ch>
|
||||
linkyard.cloud
|
||||
|
@ -14312,6 +14373,10 @@ netlify.app
|
|||
// Submitted by Trung Tran <Trung.Tran@neustar.biz>
|
||||
4u.com
|
||||
|
||||
// NGO.US Registry : https://nic.ngo.us
|
||||
// Submitted by Alstra Solutions Ltd. Networking Team <admin@alstra.org>
|
||||
ngo.us
|
||||
|
||||
// ngrok : https://ngrok.com/
|
||||
// Submitted by Alan Shreve <alan@ngrok.com>
|
||||
ngrok.app
|
||||
|
@ -14362,6 +14427,10 @@ noop.app
|
|||
// Submitted by Laurent Pellegrino <security@noticeable.io>
|
||||
noticeable.news
|
||||
|
||||
// Notion Labs, Inc : https://www.notion.so/
|
||||
// Submitted by Jess Yao <trust-core-team@makenotion.com>
|
||||
notion.site
|
||||
|
||||
// Now-DNS : https://now-dns.com
|
||||
// Submitted by Steve Russell <steve@now-dns.com>
|
||||
dnsking.ch
|
||||
|
@ -14495,8 +14564,13 @@ pcloud.host
|
|||
// Submitted by Matthew Brown <mattbrown@nyc.mn>
|
||||
nyc.mn
|
||||
|
||||
// O3O.Foundation : https://o3o.foundation/
|
||||
// Submitted by the prvcy.page Registry Team <psl@registry.prvcy.page>
|
||||
prvcy.page
|
||||
|
||||
// Observable, Inc. : https://observablehq.com
|
||||
// Submitted by Mike Bostock <dns@observablehq.com>
|
||||
observablehq.cloud
|
||||
static.observableusercontent.com
|
||||
|
||||
// Octopodal Solutions, LLC. : https://ulterius.io/
|
||||
|
@ -14624,6 +14698,7 @@ pagexl.com
|
|||
// pcarrier.ca Software Inc: https://pcarrier.ca/
|
||||
// Submitted by Pierre Carrier <pc@rrier.ca>
|
||||
*.xmit.co
|
||||
xmit.dev
|
||||
srv.us
|
||||
gh.srv.us
|
||||
gl.srv.us
|
||||
|
@ -14714,10 +14789,6 @@ xen.prgmr.com
|
|||
// Submitted by registry <lendl@nic.at>
|
||||
priv.at
|
||||
|
||||
// privacytools.io : https://www.privacytools.io/
|
||||
// Submitted by Jonah Aragon <jonah@privacytools.io>
|
||||
prvcy.page
|
||||
|
||||
// Protocol Labs : https://protocol.ai/
|
||||
// Submitted by Michael Burns <noc@protocol.ai>
|
||||
*.dweb.link
|
||||
|
@ -15062,6 +15133,10 @@ biz.ua
|
|||
co.ua
|
||||
pp.ua
|
||||
|
||||
// Sheezy.Art : https://sheezy.art
|
||||
// Submitted by Nyoom <admin@sheezy.art>
|
||||
sheezy.games
|
||||
|
||||
// Shift Crypto AG : https://shiftcrypto.ch
|
||||
// Submitted by alex <alex@shiftcrypto.ch>
|
||||
shiftcrypto.dev
|
||||
|
@ -15484,6 +15559,11 @@ dnsupdate.info
|
|||
// Submitted by Ed Moore <Ed.Moore@lib.de.us>
|
||||
lib.de.us
|
||||
|
||||
// Val Town, Inc : https://val.town/
|
||||
// Submitted by Tom MacWright <security@val.town>
|
||||
express.val.run
|
||||
web.val.run
|
||||
|
||||
// VeryPositive SIA : http://very.lv
|
||||
// Submitted by Danko Aleksejevs <danko@very.lv>
|
||||
2038.io
|
||||
|
@ -15548,6 +15628,10 @@ wedeploy.sh
|
|||
// Submitted by Jung Jin <jungseok.jin@wdc.com>
|
||||
remotewd.com
|
||||
|
||||
// Whatbox Inc. : https://whatbox.ca/
|
||||
// Submitted by Anthony Ryan <servers@whatbox.ca>
|
||||
box.ca
|
||||
|
||||
// WIARD Enterprises : https://wiardweb.com
|
||||
// Submitted by Kidd Hustle <kiddhustle@wiardweb.com>
|
||||
pages.wiardweb.com
|
||||
|
@ -15650,6 +15734,10 @@ za.org
|
|||
// Submitted by Julian Alker <security@zap-hosting.com>
|
||||
zap.cloud
|
||||
|
||||
// Zeabur : https://zeabur.com/
|
||||
// Submitted by Zeabur Team <contact@zeabur.com>
|
||||
zeabur.app
|
||||
|
||||
// Zine EOOD : https://zine.bg/
|
||||
// Submitted by Martin Angelov <martin@zine.bg>
|
||||
bss.design
|
||||
|
|
|
@ -118,7 +118,7 @@ class RoomTrackingLiveData<T> extends LiveData<T> {
|
|||
if (isActive)
|
||||
synchronized (lock) {
|
||||
if (queued > 0)
|
||||
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug,
|
||||
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug1,
|
||||
mComputeFunction + " queued=" + queued);
|
||||
else {
|
||||
queued++;
|
||||
|
|
|
@ -2,11 +2,26 @@ package com.bugsnag.android
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
|
||||
|
@ -49,12 +64,57 @@ internal class AppDataCollector(
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getProcessImportance(): String? {
|
||||
try {
|
||||
val appInfo = ActivityManager.RunningAppProcessInfo()
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||
ActivityManager.getMyMemoryState(appInfo)
|
||||
} else {
|
||||
val expectedPid = Process.myPid()
|
||||
activityManager?.runningAppProcesses
|
||||
?.find { it.pid == expectedPid }
|
||||
?.let {
|
||||
appInfo.importance = it.importance
|
||||
appInfo.pid = expectedPid
|
||||
}
|
||||
}
|
||||
|
||||
if (appInfo.pid == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return when (appInfo.importance) {
|
||||
IMPORTANCE_FOREGROUND -> "foreground"
|
||||
IMPORTANCE_FOREGROUND_SERVICE -> "foreground service"
|
||||
IMPORTANCE_TOP_SLEEPING -> "top sleeping"
|
||||
IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping"
|
||||
IMPORTANCE_VISIBLE -> "visible"
|
||||
IMPORTANCE_PERCEPTIBLE -> "perceptible"
|
||||
IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible"
|
||||
IMPORTANCE_CANT_SAVE_STATE -> "can't save state"
|
||||
IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state"
|
||||
IMPORTANCE_SERVICE -> "service"
|
||||
IMPORTANCE_CACHED -> "cached/background"
|
||||
IMPORTANCE_GONE -> "gone"
|
||||
IMPORTANCE_EMPTY -> "empty"
|
||||
REASON_PROVIDER_IN_USE -> "provider in use"
|
||||
REASON_SERVICE_IN_USE -> "service in use"
|
||||
else -> "unknown importance (${appInfo.importance})"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppDataMetadata(): MutableMap<String, Any?> {
|
||||
val map = HashMap<String, Any?>()
|
||||
map["name"] = appName
|
||||
map["activeScreen"] = sessionTracker.contextActivity
|
||||
map["lowMemory"] = memoryTrimState.isLowMemory
|
||||
map["memoryTrimLevel"] = memoryTrimState.trimLevelDescription
|
||||
map["processImportance"] = getProcessImportance()
|
||||
|
||||
populateRuntimeMemoryMetadata(map)
|
||||
|
||||
|
@ -128,6 +188,7 @@ internal class AppDataCollector(
|
|||
packageManager != null && copy != null -> {
|
||||
packageManager.getApplicationLabel(copy).toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +217,7 @@ internal class AppDataCollector(
|
|||
VERSION.SDK_INT >= VERSION_CODES.P -> {
|
||||
Application.getProcessName()
|
||||
}
|
||||
|
||||
else -> {
|
||||
// see https://stackoverflow.com/questions/19631894
|
||||
val clz = Class.forName("android.app.ActivityThread")
|
||||
|
@ -179,5 +241,7 @@ internal class AppDataCollector(
|
|||
* good approximation for how long the app has been running.
|
||||
*/
|
||||
fun getDurationMs(): Long = SystemClock.elapsedRealtime() - startTimeMs
|
||||
|
||||
private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,12 +29,14 @@ internal class BugsnagEventMapper(
|
|||
event.userImpl = convertUser(map.readEntry("user"))
|
||||
|
||||
// populate metadata
|
||||
val metadataMap: Map<String, Map<String, Any?>> = map.readEntry("metaData")
|
||||
val metadataMap: Map<String, Map<String, Any?>> =
|
||||
(map["metaData"] as? Map<String, Map<String, Any?>>).orEmpty()
|
||||
metadataMap.forEach { (key, value) ->
|
||||
event.addMetadata(key, value)
|
||||
}
|
||||
|
||||
val featureFlagsList: List<Map<String, Any?>> = map.readEntry("featureFlags")
|
||||
val featureFlagsList: List<Map<String, Any?>> =
|
||||
(map["featureFlags"] as? List<Map<String, Any?>>).orEmpty()
|
||||
featureFlagsList.forEach { featureFlagMap ->
|
||||
event.addFeatureFlag(
|
||||
featureFlagMap.readEntry("featureFlag"),
|
||||
|
@ -43,7 +45,8 @@ internal class BugsnagEventMapper(
|
|||
}
|
||||
|
||||
// populate breadcrumbs
|
||||
val breadcrumbList: List<MutableMap<String, Any?>> = map.readEntry("breadcrumbs")
|
||||
val breadcrumbList: List<MutableMap<String, Any?>> =
|
||||
(map["breadcrumbs"] as? List<MutableMap<String, Any?>>).orEmpty()
|
||||
breadcrumbList.mapTo(event.breadcrumbs) {
|
||||
Breadcrumb(
|
||||
convertBreadcrumbInternal(it),
|
||||
|
@ -226,8 +229,7 @@ internal class BugsnagEventMapper(
|
|||
is T -> return value
|
||||
null -> throw IllegalStateException("cannot find json property '$key'")
|
||||
else -> throw IllegalArgumentException(
|
||||
"json property '$key' not " +
|
||||
"of expected type, found ${value.javaClass.name}"
|
||||
"json property '$key' not of expected type, found ${value.javaClass.name}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ internal class ConfigInternal(
|
|||
var releaseStage: String? = null
|
||||
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
|
||||
var persistUser: Boolean = true
|
||||
var generateAnonymousId: Boolean = true
|
||||
|
||||
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS
|
||||
|
||||
|
|
|
@ -178,6 +178,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
|||
impl.setPersistUser(persistUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
|
||||
*
|
||||
* If disabled, any device ID that has been persisted will not be retrieved, and no new
|
||||
* device ID will be generated or stored
|
||||
*/
|
||||
public boolean getGenerateAnonymousId() {
|
||||
return impl.getGenerateAnonymousId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
|
||||
*
|
||||
* If disabled, any device ID that has been persisted will not be retrieved, and no new
|
||||
* device ID will be generated or stored
|
||||
*/
|
||||
public void setGenerateAnonymousId(boolean generateAnonymousId) {
|
||||
impl.setGenerateAnonymousId(generateAnonymousId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory where event and session JSON payloads should be persisted if a network
|
||||
* request is not successful. If you use Bugsnag in multiple processes, then a unique
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.bugsnag.android
|
||||
|
||||
import android.content.Context
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
|
@ -8,18 +9,20 @@ import java.util.UUID
|
|||
* This class is responsible for persisting and retrieving the device ID and internal device ID,
|
||||
* which uniquely identify this device in various contexts.
|
||||
*/
|
||||
internal class DeviceIdStore @JvmOverloads constructor(
|
||||
internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor(
|
||||
context: Context,
|
||||
deviceIdfile: File = File(context.filesDir, "device-id"),
|
||||
deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||
internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"),
|
||||
internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||
private val sharedPrefMigrator: SharedPrefMigrator,
|
||||
config: ImmutableConfig,
|
||||
logger: Logger
|
||||
) {
|
||||
|
||||
private val persistence: DeviceIdPersistence
|
||||
private val internalPersistence: DeviceIdPersistence
|
||||
private val generateId = config.generateAnonymousId
|
||||
|
||||
init {
|
||||
persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
|
||||
|
@ -35,6 +38,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
|
|||
* be used. If no value is present then a random UUID will be generated and persisted.
|
||||
*/
|
||||
fun loadDeviceId(): String? {
|
||||
// If generateAnonymousId = false, return null
|
||||
// so that a previously persisted device ID is not returned,
|
||||
// or a new one is not generated and persisted
|
||||
if (!generateId) {
|
||||
return null
|
||||
}
|
||||
var result = persistence.loadDeviceId(false)
|
||||
if (result != null) {
|
||||
return result
|
||||
|
@ -47,6 +56,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
fun loadInternalDeviceId(): String? {
|
||||
// If generateAnonymousId = false, return null
|
||||
// so that a previously persisted device ID is not returned,
|
||||
// or a new one is not generated and persisted
|
||||
if (!generateId) {
|
||||
return null
|
||||
}
|
||||
return internalPersistence.loadDeviceId(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,113 @@
|
|||
package com.bugsnag.android
|
||||
|
||||
import java.io.IOException
|
||||
import kotlin.math.max
|
||||
|
||||
internal class FeatureFlags(
|
||||
internal val store: MutableMap<String, String?> = mutableMapOf()
|
||||
internal class FeatureFlags private constructor(
|
||||
@Volatile
|
||||
private var flags: Array<FeatureFlag>
|
||||
) : JsonStream.Streamable, FeatureFlagAware {
|
||||
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
|
||||
|
||||
@Synchronized override fun addFeatureFlag(name: String) {
|
||||
/*
|
||||
* Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are
|
||||
* key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's
|
||||
* internal locking) for us to be sure we are not adding duplicates.
|
||||
*
|
||||
* This class aims to have similar performance while also ensuring that the FeatureFlag object
|
||||
* themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot
|
||||
* operation for when an Event is created.
|
||||
*
|
||||
* It's assumed that *most* FeatureFlags will be added up-front, or during the normal app
|
||||
* lifecycle (not during an Event).
|
||||
*
|
||||
* As such a copy-on-write structure allows an Event to simply capture a reference to the
|
||||
* "snapshot" of FeatureFlags that were active when the Event was created.
|
||||
*/
|
||||
|
||||
constructor() : this(emptyArray<FeatureFlag>())
|
||||
|
||||
override fun addFeatureFlag(name: String) {
|
||||
addFeatureFlag(name, null)
|
||||
}
|
||||
|
||||
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
|
||||
store[name] = variant ?: emptyVariant
|
||||
}
|
||||
override fun addFeatureFlag(name: String, variant: String?) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
val index = flagArray.indexOfFirst { it.name == name }
|
||||
|
||||
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
||||
featureFlags.forEach { (name, variant) ->
|
||||
addFeatureFlag(name, variant)
|
||||
flags = when {
|
||||
// this is a new FeatureFlag
|
||||
index == -1 -> flagArray + FeatureFlag(name, variant)
|
||||
|
||||
// this is a change to an existing FeatureFlag
|
||||
flagArray[index].variant != variant -> flagArray.copyOf().also {
|
||||
// replace the existing FeatureFlag in-place
|
||||
it[index] = FeatureFlag(name, variant)
|
||||
}
|
||||
|
||||
// no actual change, so we return
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized override fun clearFeatureFlag(name: String) {
|
||||
store.remove(name)
|
||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
|
||||
val newFlags = ArrayList<FeatureFlag>(
|
||||
// try to guess a reasonable upper-bound for the output array
|
||||
if (featureFlags is Collection<*>) flagArray.size + featureFlags.size
|
||||
else max(flagArray.size * 2, flagArray.size)
|
||||
)
|
||||
|
||||
newFlags.addAll(flagArray)
|
||||
|
||||
featureFlags.forEach { (name, variant) ->
|
||||
val existingIndex = newFlags.indexOfFirst { it.name == name }
|
||||
when (existingIndex) {
|
||||
// add a new flag to the end of the list
|
||||
-1 -> newFlags.add(FeatureFlag(name, variant))
|
||||
// replace the existing flag
|
||||
else -> newFlags[existingIndex] = FeatureFlag(name, variant)
|
||||
}
|
||||
}
|
||||
|
||||
flags = newFlags.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized override fun clearFeatureFlags() {
|
||||
store.clear()
|
||||
override fun clearFeatureFlag(name: String) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
val index = flagArray.indexOfFirst { it.name == name }
|
||||
if (index == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
val out = arrayOfNulls<FeatureFlag>(flagArray.size - 1)
|
||||
flagArray.copyInto(out, 0, 0, index)
|
||||
flagArray.copyInto(out, index, index + 1)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
flags = out as Array<FeatureFlag>
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearFeatureFlags() {
|
||||
synchronized(this) {
|
||||
flags = emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toStream(stream: JsonStream) {
|
||||
val storeCopy = synchronized(this) { store.toMap() }
|
||||
val storeCopy = flags
|
||||
stream.beginArray()
|
||||
storeCopy.forEach { (name, variant) ->
|
||||
stream.beginObject()
|
||||
stream.name("featureFlag").value(name)
|
||||
if (variant != emptyVariant) {
|
||||
if (variant != null) {
|
||||
stream.name("variant").value(variant)
|
||||
}
|
||||
stream.endObject()
|
||||
|
@ -44,9 +115,7 @@ internal class FeatureFlags(
|
|||
stream.endArray()
|
||||
}
|
||||
|
||||
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
|
||||
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
|
||||
}
|
||||
fun toList(): List<FeatureFlag> = flags.map { (name, variant) -> FeatureFlag(name, variant) }
|
||||
|
||||
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
|
||||
fun copy() = FeatureFlags(flags)
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ class JsonWriter implements Closeable, Flushable {
|
|||
*/
|
||||
private static final String[] REPLACEMENT_CHARS;
|
||||
private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
|
||||
|
||||
static {
|
||||
REPLACEMENT_CHARS = new String[128];
|
||||
for (int i = 0; i <= 0x1f; i++) {
|
||||
|
@ -165,11 +166,14 @@ class JsonWriter implements Closeable, Flushable {
|
|||
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
|
||||
}
|
||||
|
||||
/** The output data, containing at most one top-level array or object. */
|
||||
/**
|
||||
* The output data, containing at most one top-level array or object.
|
||||
*/
|
||||
private final Writer out;
|
||||
|
||||
private int[] stack = new int[32];
|
||||
private int stackSize = 0;
|
||||
|
||||
{
|
||||
push(EMPTY_DOCUMENT);
|
||||
}
|
||||
|
@ -337,7 +341,7 @@ class JsonWriter implements Closeable, Flushable {
|
|||
* given bracket.
|
||||
*/
|
||||
private JsonWriter close(int empty, int nonempty, String closeBracket)
|
||||
throws IOException {
|
||||
throws IOException {
|
||||
int context = peek();
|
||||
if (context != nonempty && context != empty) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
|
@ -437,7 +441,7 @@ class JsonWriter implements Closeable, Flushable {
|
|||
}
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.append(value);
|
||||
out.write(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -490,17 +494,18 @@ class JsonWriter implements Closeable, Flushable {
|
|||
/**
|
||||
* Encodes {@code value}.
|
||||
*
|
||||
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
|
||||
* {@link Double#isInfinite() infinities}.
|
||||
* @param value a finite value.
|
||||
* @return this writer.
|
||||
*/
|
||||
public JsonWriter value(double value) throws IOException {
|
||||
writeDeferredName();
|
||||
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
|
||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||
// omit these values instead of attempting to write them
|
||||
deferredName = null;
|
||||
} else {
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.write(Double.toString(value));
|
||||
}
|
||||
beforeValue();
|
||||
out.append(Double.toString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -520,7 +525,7 @@ class JsonWriter implements Closeable, Flushable {
|
|||
* Encodes {@code value}.
|
||||
*
|
||||
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
|
||||
* {@link Double#isInfinite() infinities}.
|
||||
* {@link Double#isInfinite() infinities}.
|
||||
* @return this writer.
|
||||
*/
|
||||
public JsonWriter value(Number value) throws IOException {
|
||||
|
@ -528,14 +533,16 @@ class JsonWriter implements Closeable, Flushable {
|
|||
return nullValue();
|
||||
}
|
||||
|
||||
writeDeferredName();
|
||||
String string = value.toString();
|
||||
if (!lenient
|
||||
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
|
||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
|
||||
// omit this value
|
||||
deferredName = null;
|
||||
} else {
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.write(string);
|
||||
}
|
||||
beforeValue();
|
||||
out.append(string);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -634,7 +641,7 @@ class JsonWriter implements Closeable, Flushable {
|
|||
case NONEMPTY_DOCUMENT:
|
||||
if (!lenient) {
|
||||
throw new IllegalStateException(
|
||||
"JSON must have only one top-level value.");
|
||||
"JSON must have only one top-level value.");
|
||||
}
|
||||
// fall-through
|
||||
case EMPTY_DOCUMENT: // first in document
|
||||
|
@ -647,12 +654,12 @@ class JsonWriter implements Closeable, Flushable {
|
|||
break;
|
||||
|
||||
case NONEMPTY_ARRAY: // another in array
|
||||
out.append(',');
|
||||
out.write(',');
|
||||
newline();
|
||||
break;
|
||||
|
||||
case DANGLING_NAME: // value for name
|
||||
out.append(separator);
|
||||
out.write(separator);
|
||||
replaceTop(NONEMPTY_OBJECT);
|
||||
break;
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ internal class ManifestConfigLoader {
|
|||
private const val AUTO_DETECT_ERRORS = "$BUGSNAG_NS.AUTO_DETECT_ERRORS"
|
||||
private const val PERSIST_USER = "$BUGSNAG_NS.PERSIST_USER"
|
||||
private const val SEND_THREADS = "$BUGSNAG_NS.SEND_THREADS"
|
||||
private const val GENERATE_ANONYMOUS_ID = "$BUGSNAG_NS.GENERATE_ANONYMOUS_ID"
|
||||
|
||||
// endpoints
|
||||
private const val ENDPOINT_NOTIFY = "$BUGSNAG_NS.ENDPOINT_NOTIFY"
|
||||
|
@ -108,6 +109,7 @@ internal class ManifestConfigLoader {
|
|||
autoTrackSessions = data.getBoolean(AUTO_TRACK_SESSIONS, autoTrackSessions)
|
||||
autoDetectErrors = data.getBoolean(AUTO_DETECT_ERRORS, autoDetectErrors)
|
||||
persistUser = data.getBoolean(PERSIST_USER, persistUser)
|
||||
generateAnonymousId = data.getBoolean(GENERATE_ANONYMOUS_ID, generateAnonymousId)
|
||||
|
||||
val str = data.getString(SEND_THREADS)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.io.IOException
|
|||
*/
|
||||
class Notifier @JvmOverloads constructor(
|
||||
var name: String = "Android Bugsnag Notifier",
|
||||
var version: String = "6.1.0",
|
||||
var version: String = "6.4.0",
|
||||
var url: String = "https://bugsnag.com"
|
||||
) : JsonStream.Streamable {
|
||||
|
||||
|
|
|
@ -11,9 +11,11 @@ internal class ObjectJsonStreamer {
|
|||
companion object {
|
||||
internal const val REDACTED_PLACEHOLDER = "[REDACTED]"
|
||||
internal const val OBJECT_PLACEHOLDER = "[OBJECT]"
|
||||
|
||||
internal val DEFAULT_REDACTED_KEYS = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
var redactedKeys = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
|
||||
var redactedKeys = DEFAULT_REDACTED_KEYS
|
||||
|
||||
// Write complex/nested values to a JsonStreamer
|
||||
@Throws(IOException::class)
|
||||
|
|
|
@ -36,6 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
|
|||
private volatile Session currentSession = null;
|
||||
final BackgroundTaskService backgroundTaskService;
|
||||
final Logger logger;
|
||||
private boolean shouldSuppressFirstAutoSession = false;
|
||||
|
||||
SessionTracker(ImmutableConfig configuration,
|
||||
CallbackState callbackState,
|
||||
|
@ -76,7 +77,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
|
|||
@VisibleForTesting
|
||||
Session startNewSession(@NonNull Date date, @Nullable User user,
|
||||
boolean autoCaptured) {
|
||||
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
|
||||
if (shouldDiscardSession(autoCaptured)) {
|
||||
return null;
|
||||
}
|
||||
String id = UUID.randomUUID().toString();
|
||||
|
@ -92,12 +93,29 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
|
|||
}
|
||||
|
||||
Session startSession(boolean autoCaptured) {
|
||||
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
|
||||
if (shouldDiscardSession(autoCaptured)) {
|
||||
return null;
|
||||
}
|
||||
return startNewSession(new Date(), client.getUser(), autoCaptured);
|
||||
}
|
||||
|
||||
private boolean shouldDiscardSession(boolean autoCaptured) {
|
||||
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
|
||||
return true;
|
||||
} else {
|
||||
Session existingSession = currentSession;
|
||||
if (autoCaptured
|
||||
&& existingSession != null
|
||||
&& !existingSession.isAutoCaptured()
|
||||
&& shouldSuppressFirstAutoSession) {
|
||||
shouldSuppressFirstAutoSession = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void pauseSession() {
|
||||
Session session = currentSession;
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ internal class StorageModule(
|
|||
DeviceIdStore(
|
||||
appContext,
|
||||
sharedPrefMigrator = sharedPrefMigrator,
|
||||
logger = logger
|
||||
logger = logger,
|
||||
config = immutableConfig
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,9 @@ internal class UserStore @JvmOverloads constructor(
|
|||
* [Configuration.getPersistUser] is true.
|
||||
*
|
||||
* If no user is stored on disk, then a default [User] is used which uses the device ID
|
||||
* as its ID.
|
||||
* as its ID (unless the generateAnonymousId config option is set to false, in which case the
|
||||
* device ID and therefore the user ID is set to
|
||||
* null).
|
||||
*
|
||||
* The [UserState] provides a mechanism for observing value changes to its user property,
|
||||
* so to avoid interfering with this the method should only be called once for each [Client].
|
||||
|
@ -46,6 +48,8 @@ internal class UserStore @JvmOverloads constructor(
|
|||
|
||||
val userState = when {
|
||||
loadedUser != null && validUser(loadedUser) -> UserState(loadedUser)
|
||||
// if generateAnonymousId config option is false, the deviceId should already be null
|
||||
// here
|
||||
else -> UserState(User(deviceId, null, null))
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ data class ImmutableConfig(
|
|||
val persistenceDirectory: Lazy<File>,
|
||||
val sendLaunchCrashesSynchronously: Boolean,
|
||||
val attemptDeliveryOnCrash: Boolean,
|
||||
val generateAnonymousId: Boolean,
|
||||
|
||||
// results cached here to avoid unnecessary lookups in Client.
|
||||
val packageInfo: PackageInfo?,
|
||||
|
@ -166,6 +167,7 @@ internal fun convertToImmutableConfig(
|
|||
delivery = config.delivery,
|
||||
endpoints = config.endpoints,
|
||||
persistUser = config.persistUser,
|
||||
generateAnonymousId = config.generateAnonymousId,
|
||||
launchDurationMillis = config.launchDurationMillis,
|
||||
logger = config.logger!!,
|
||||
maxBreadcrumbs = config.maxBreadcrumbs,
|
||||
|
|
|
@ -39,7 +39,7 @@ public abstract class BinaryConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<byte[]> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(Base64Reader);
|
||||
return reader.deserializeCollectionCustom(Base64Reader);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<byte[]> res) throws IOException {
|
||||
|
@ -48,7 +48,7 @@ public abstract class BinaryConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<byte[]> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(Base64Reader);
|
||||
return reader.deserializeNullableCollectionCustom(Base64Reader);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<byte[]> res) throws IOException {
|
||||
|
|
|
@ -110,7 +110,7 @@ public abstract class BoolConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Boolean> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(READER);
|
||||
return reader.deserializeCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<Boolean> res) throws IOException {
|
||||
|
@ -119,7 +119,7 @@ public abstract class BoolConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Boolean> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(READER);
|
||||
return reader.deserializeNullableCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Boolean> res) throws IOException {
|
||||
|
|
|
@ -804,7 +804,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
for (ClassLoader loader : loaders) {
|
||||
try {
|
||||
Class<?> external = loader.loadClass(name);
|
||||
Configuration instance = (Configuration) external.newInstance();
|
||||
Configuration instance = (Configuration) external.getDeclaredConstructor().newInstance();
|
||||
instance.configure(json);
|
||||
} catch (NoClassDefFoundError ignore) {
|
||||
} catch (Exception ignore) {
|
||||
|
@ -813,6 +813,8 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
|
||||
static void registerJavaSpecifics(final DslJson json) {
|
||||
json.registerReader(Element.class, XmlConverter.Reader);
|
||||
json.registerWriter(Element.class, XmlConverter.Writer);
|
||||
}
|
||||
|
||||
private final Map<Type, Object> defaults = new ConcurrentHashMap<Type, Object>();
|
||||
|
@ -1653,7 +1655,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject<?> contentReader = tryFindReader(content);
|
||||
if (contentReader != null) {
|
||||
final ArrayList<?> result = json.deserializeNullableCollection(contentReader);
|
||||
final ArrayList<?> result = json.deserializeNullableCollectionCustom(contentReader);
|
||||
if (container.isArray()) {
|
||||
return returnAsArray(content, result);
|
||||
}
|
||||
|
@ -1673,7 +1675,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject<?> contentReader = tryFindReader(content);
|
||||
if (contentReader != null) {
|
||||
final ArrayList<?> result = json.deserializeNullableCollection(contentReader);
|
||||
final ArrayList<?> result = json.deserializeNullableCollectionCustom(contentReader);
|
||||
return returnAsArray(content, result);
|
||||
}
|
||||
}
|
||||
|
@ -1768,7 +1770,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject<?> simpleReader = tryFindReader(manifest);
|
||||
if (simpleReader != null) {
|
||||
return json.deserializeNullableCollection(simpleReader);
|
||||
return json.deserializeNullableCollectionCustom(simpleReader);
|
||||
}
|
||||
if (fallback != null) {
|
||||
final Object array = Array.newInstance(manifest, 0);
|
||||
|
@ -1883,7 +1885,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject simpleReader = tryFindReader(manifest);
|
||||
if (simpleReader != null) {
|
||||
return json.deserializeNullableCollection(simpleReader);
|
||||
return json.deserializeNullableCollectionCustom(simpleReader);
|
||||
}
|
||||
if (fallback != null) {
|
||||
final Object array = Array.newInstance(manifest, 0);
|
||||
|
@ -2009,7 +2011,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject<?> simpleElementReader = tryFindReader(elementManifest);
|
||||
if (simpleElementReader != null) {
|
||||
List<?> list = json.deserializeNullableCollection(simpleElementReader);
|
||||
List<?> list = json.deserializeNullableCollectionCustom(simpleElementReader);
|
||||
return (TResult) convertResultToArray(elementManifest, list);
|
||||
}
|
||||
}
|
||||
|
@ -2267,7 +2269,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
|
|||
}
|
||||
final JsonReader.ReadObject<?> simpleReader = tryFindReader(manifest);
|
||||
if (simpleReader != null) {
|
||||
return json.iterateOver(simpleReader);
|
||||
return json.iterateOverCustom(simpleReader);
|
||||
}
|
||||
if (fallback != null) {
|
||||
final Object array = Array.newInstance(manifest, 0);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.bugsnag.android.repackaged.dslplatform.json;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
|
||||
class ExternalConverterAnalyzer {
|
||||
|
@ -19,14 +20,16 @@ class ExternalConverterAnalyzer {
|
|||
try {
|
||||
Class<?> converterClass = cl.loadClass(ccn);
|
||||
if (!Configuration.class.isAssignableFrom(converterClass)) continue;
|
||||
Configuration converter = (Configuration) converterClass.newInstance();
|
||||
Configuration converter = (Configuration) converterClass.getDeclaredConstructor().newInstance();
|
||||
converter.configure(dslJson);
|
||||
return true;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
} catch (IllegalAccessException ignored) {
|
||||
} catch (InstantiationException ignored) {
|
||||
}
|
||||
}
|
||||
} catch (InvocationTargetException e) {
|
||||
} catch (NoSuchMethodException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1594,12 +1594,13 @@ public final class JsonReader<TContext> {
|
|||
return res.toArray(emptyArray);
|
||||
}
|
||||
|
||||
public final <T, S extends T> ArrayList<T> deserializeCollection(final ReadObject<S> readObject) throws IOException {
|
||||
public final <T, S extends T> ArrayList<T> deserializeCollectionCustom(final ReadObject<S> readObject) throws IOException {
|
||||
final ArrayList<T> res = new ArrayList<T>(4);
|
||||
deserializeCollection(readObject, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressWarnings("overloads")
|
||||
public final <T, S extends T> void deserializeCollection(final ReadObject<S> readObject, final Collection<T> res) throws IOException {
|
||||
res.add(readObject.read(this));
|
||||
while (getNextToken() == ',') {
|
||||
|
@ -1609,12 +1610,13 @@ public final class JsonReader<TContext> {
|
|||
checkArrayEnd();
|
||||
}
|
||||
|
||||
public final <T, S extends T> ArrayList<T> deserializeNullableCollection(final ReadObject<S> readObject) throws IOException {
|
||||
public final <T, S extends T> ArrayList<T> deserializeNullableCollectionCustom(final ReadObject<S> readObject) throws IOException {
|
||||
final ArrayList<T> res = new ArrayList<T>(4);
|
||||
deserializeNullableCollection(readObject, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressWarnings("overloads")
|
||||
public final <T, S extends T> void deserializeNullableCollection(final ReadObject<S> readObject, final Collection<T> res) throws IOException {
|
||||
if (wasNull()) {
|
||||
res.add(null);
|
||||
|
@ -1638,6 +1640,7 @@ public final class JsonReader<TContext> {
|
|||
return res;
|
||||
}
|
||||
|
||||
@SuppressWarnings("overloads")
|
||||
public final <T extends JsonObject> void deserializeCollection(final ReadJsonObject<T> readObject, final Collection<T> res) throws IOException {
|
||||
if (last == '{') {
|
||||
getNextToken();
|
||||
|
@ -1658,6 +1661,7 @@ public final class JsonReader<TContext> {
|
|||
return res;
|
||||
}
|
||||
|
||||
@SuppressWarnings("overloads")
|
||||
public final <T extends JsonObject> void deserializeNullableCollection(final ReadJsonObject<T> readObject, final Collection<T> res) throws IOException {
|
||||
if (last == '{') {
|
||||
getNextToken();
|
||||
|
@ -1676,7 +1680,7 @@ public final class JsonReader<TContext> {
|
|||
checkArrayEnd();
|
||||
}
|
||||
|
||||
public final <T> Iterator<T> iterateOver(final JsonReader.ReadObject<T> reader) {
|
||||
public final <T> Iterator<T> iterateOverCustom(final JsonReader.ReadObject<T> reader) {
|
||||
return new WithReader<T>(reader, this);
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ public abstract class MapConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Map<String, String>> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(TypedMapReader);
|
||||
return reader.deserializeCollectionCustom(TypedMapReader);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<Map<String, String>> res) throws IOException {
|
||||
|
@ -79,7 +79,7 @@ public abstract class MapConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Map<String, String>> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(TypedMapReader);
|
||||
return reader.deserializeNullableCollectionCustom(TypedMapReader);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Map<String, String>> res) throws IOException {
|
||||
|
|
|
@ -56,7 +56,7 @@ public abstract class NetConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<URI> deserializeUriCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(UriReader);
|
||||
return reader.deserializeCollectionCustom(UriReader);
|
||||
}
|
||||
|
||||
public static void deserializeUriCollection(final JsonReader reader, final Collection<URI> res) throws IOException {
|
||||
|
@ -65,7 +65,7 @@ public abstract class NetConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<URI> deserializeUriNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(UriReader);
|
||||
return reader.deserializeNullableCollectionCustom(UriReader);
|
||||
}
|
||||
|
||||
public static void deserializeUriNullableCollection(final JsonReader reader, final Collection<URI> res) throws IOException {
|
||||
|
@ -92,7 +92,7 @@ public abstract class NetConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<InetAddress> deserializeIpCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(AddressReader);
|
||||
return reader.deserializeCollectionCustom(AddressReader);
|
||||
}
|
||||
|
||||
public static void deserializeIpCollection(final JsonReader reader, final Collection<InetAddress> res) throws IOException {
|
||||
|
@ -101,7 +101,7 @@ public abstract class NetConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<InetAddress> deserializeIpNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(AddressReader);
|
||||
return reader.deserializeNullableCollectionCustom(AddressReader);
|
||||
}
|
||||
|
||||
public static void deserializeIpNullableCollection(final JsonReader reader, final Collection<InetAddress> res) throws IOException {
|
||||
|
|
|
@ -570,7 +570,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Double> deserializeDoubleCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(DOUBLE_READER);
|
||||
return reader.deserializeCollectionCustom(DOUBLE_READER);
|
||||
}
|
||||
|
||||
public static void deserializeDoubleCollection(final JsonReader reader, final Collection<Double> res) throws IOException {
|
||||
|
@ -579,7 +579,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Double> deserializeDoubleNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(DOUBLE_READER);
|
||||
return reader.deserializeNullableCollectionCustom(DOUBLE_READER);
|
||||
}
|
||||
|
||||
public static void deserializeDoubleNullableCollection(final JsonReader reader, final Collection<Double> res) throws IOException {
|
||||
|
@ -772,7 +772,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Float> deserializeFloatCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(FLOAT_READER);
|
||||
return reader.deserializeCollectionCustom(FLOAT_READER);
|
||||
}
|
||||
|
||||
public static void deserializeFloatCollection(final JsonReader reader, Collection<Float> res) throws IOException {
|
||||
|
@ -781,7 +781,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Float> deserializeFloatNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(FLOAT_READER);
|
||||
return reader.deserializeNullableCollectionCustom(FLOAT_READER);
|
||||
}
|
||||
|
||||
public static void deserializeFloatNullableCollection(final JsonReader reader, final Collection<Float> res) throws IOException {
|
||||
|
@ -981,7 +981,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Integer> deserializeIntCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(INT_READER);
|
||||
return reader.deserializeCollectionCustom(INT_READER);
|
||||
}
|
||||
|
||||
public static int[] deserializeIntArray(final JsonReader reader) throws IOException {
|
||||
|
@ -1080,7 +1080,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Short> deserializeShortNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(SHORT_READER);
|
||||
return reader.deserializeNullableCollectionCustom(SHORT_READER);
|
||||
}
|
||||
|
||||
public static void deserializeShortNullableCollection(final JsonReader reader, final Collection<Short> res) throws IOException {
|
||||
|
@ -1093,7 +1093,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Integer> deserializeIntNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(INT_READER);
|
||||
return reader.deserializeNullableCollectionCustom(INT_READER);
|
||||
}
|
||||
|
||||
public static void deserializeIntNullableCollection(final JsonReader reader, final Collection<Integer> res) throws IOException {
|
||||
|
@ -1317,7 +1317,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Long> deserializeLongCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(LONG_READER);
|
||||
return reader.deserializeCollectionCustom(LONG_READER);
|
||||
}
|
||||
|
||||
public static void deserializeLongCollection(final JsonReader reader, final Collection<Long> res) throws IOException {
|
||||
|
@ -1326,7 +1326,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Long> deserializeLongNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(LONG_READER);
|
||||
return reader.deserializeNullableCollectionCustom(LONG_READER);
|
||||
}
|
||||
|
||||
public static void deserializeLongNullableCollection(final JsonReader reader, final Collection<Long> res) throws IOException {
|
||||
|
@ -1682,7 +1682,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<BigDecimal> deserializeDecimalCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(DecimalReader);
|
||||
return reader.deserializeCollectionCustom(DecimalReader);
|
||||
}
|
||||
|
||||
public static void deserializeDecimalCollection(final JsonReader reader, final Collection<BigDecimal> res) throws IOException {
|
||||
|
@ -1691,7 +1691,7 @@ public abstract class NumberConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<BigDecimal> deserializeDecimalNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(DecimalReader);
|
||||
return reader.deserializeNullableCollectionCustom(DecimalReader);
|
||||
}
|
||||
|
||||
public static void deserializeDecimalNullableCollection(final JsonReader reader, final Collection<BigDecimal> res) throws IOException {
|
||||
|
|
|
@ -117,7 +117,7 @@ public abstract class ObjectConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Map<String, Object>> deserializeMapCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(TypedMapReader);
|
||||
return reader.deserializeCollectionCustom(TypedMapReader);
|
||||
}
|
||||
|
||||
public static void deserializeMapCollection(final JsonReader reader, final Collection<Map<String, Object>> res) throws IOException {
|
||||
|
@ -126,7 +126,7 @@ public abstract class ObjectConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Map<String, Object>> deserializeNullableMapCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(TypedMapReader);
|
||||
return reader.deserializeNullableCollectionCustom(TypedMapReader);
|
||||
}
|
||||
|
||||
public static void deserializeNullableMapCollection(final JsonReader reader, final Collection<Map<String, Object>> res) throws IOException {
|
||||
|
|
|
@ -89,7 +89,7 @@ public abstract class StringConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<String> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(READER);
|
||||
return reader.deserializeCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<String> res) throws IOException {
|
||||
|
@ -98,7 +98,7 @@ public abstract class StringConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<String> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(READER);
|
||||
return reader.deserializeNullableCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<String> res) throws IOException {
|
||||
|
|
|
@ -180,7 +180,7 @@ public abstract class UUIDConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<UUID> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollection(READER);
|
||||
return reader.deserializeCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<UUID> res) throws IOException {
|
||||
|
@ -189,7 +189,7 @@ public abstract class UUIDConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<UUID> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollection(READER);
|
||||
return reader.deserializeNullableCollectionCustom(READER);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<UUID> res) throws IOException {
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
package com.bugsnag.android.repackaged.dslplatform.json;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.w3c.dom.*;
|
||||
import org.w3c.dom.ls.DOMImplementationLS;
|
||||
import org.w3c.dom.ls.LSOutput;
|
||||
import org.w3c.dom.ls.LSSerializer;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings
|
||||
public abstract class XmlConverter {
|
||||
|
||||
static final JsonReader.ReadObject<Element> Reader = new JsonReader.ReadObject<Element>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Element read(JsonReader reader) throws IOException {
|
||||
return reader.wasNull() ? null : deserialize(reader);
|
||||
}
|
||||
};
|
||||
static final JsonWriter.WriteObject<Element> Writer = new JsonWriter.WriteObject<Element>() {
|
||||
@Override
|
||||
public void write(JsonWriter writer, @Nullable Element value) {
|
||||
serializeNullable(value, writer);
|
||||
}
|
||||
};
|
||||
|
||||
private static final DocumentBuilder documentBuilder;
|
||||
|
||||
static {
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
documentBuilder = dbFactory.newDocumentBuilder();
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void serializeNullable(@Nullable final Element value, final JsonWriter sw) {
|
||||
if (value == null)
|
||||
sw.writeNull();
|
||||
else
|
||||
serialize(value, sw);
|
||||
}
|
||||
|
||||
public static void serialize(final Element value, final JsonWriter sw) {
|
||||
Document document = value.getOwnerDocument();
|
||||
DOMImplementationLS domImplLS = (DOMImplementationLS) document.getImplementation();
|
||||
LSSerializer serializer = domImplLS.createLSSerializer();
|
||||
LSOutput lsOutput = domImplLS.createLSOutput();
|
||||
lsOutput.setEncoding("UTF-8");
|
||||
StringWriter writer = new StringWriter();
|
||||
lsOutput.setCharacterStream(writer);
|
||||
serializer.write(document, lsOutput);
|
||||
StringConverter.serialize(writer.toString(), sw);
|
||||
}
|
||||
|
||||
public static Element deserialize(final JsonReader reader) throws IOException {
|
||||
if (reader.last() == '"') {
|
||||
try {
|
||||
InputSource source = new InputSource(new StringReader(reader.readString()));
|
||||
return documentBuilder.parse(source).getDocumentElement();
|
||||
} catch (SAXException ex) {
|
||||
throw reader.newParseErrorAt("Invalid XML value", 0, ex);
|
||||
}
|
||||
} else {
|
||||
final Map<String, Object> map = ObjectConverter.deserializeMap(reader);
|
||||
return mapToXml(map);
|
||||
}
|
||||
}
|
||||
|
||||
public static Element mapToXml(final Map<String, Object> map) throws IOException {
|
||||
final Set<String> xmlRootElementNames = map.keySet();
|
||||
if (xmlRootElementNames.size() > 1) {
|
||||
throw ParsingException.create("Invalid XML. Expecting root element", true);
|
||||
}
|
||||
final String rootName = xmlRootElementNames.iterator().next();
|
||||
final Document document = createDocument();
|
||||
final Element rootElement = document.createElement(rootName);
|
||||
document.appendChild(rootElement);
|
||||
buildXmlFromHashMap(document, rootElement, map.get(rootName));
|
||||
return rootElement;
|
||||
}
|
||||
|
||||
private static synchronized Document createDocument() {
|
||||
try {
|
||||
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
final DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
return builder.newDocument();
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new ConfigurationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TEXT_NODE_TAG = "#text";
|
||||
private static final String COMMENT_NODE_TAG = "#comment";
|
||||
private static final String CDATA_NODE_TAG = "#cdata-section";
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void buildXmlFromHashMap(
|
||||
final Document doc,
|
||||
final Element subtreeRootElement,
|
||||
@Nullable final Object elementContent) {
|
||||
if (elementContent instanceof HashMap) {
|
||||
final HashMap<String, Object> elementContentMap = (HashMap<String, Object>) elementContent;
|
||||
for (final Map.Entry<String, Object> childEntry : elementContentMap.entrySet()) {
|
||||
final String key = childEntry.getKey();
|
||||
if (key.startsWith("@")) {
|
||||
subtreeRootElement.setAttribute(key.substring(1), childEntry.getValue().toString());
|
||||
} else if (key.startsWith("#")) {
|
||||
if (key.equals(TEXT_NODE_TAG)) {
|
||||
if (childEntry.getValue() instanceof List) {
|
||||
buildTextNodeList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
|
||||
} else {
|
||||
final Node textNode = doc.createTextNode(childEntry.getValue().toString());
|
||||
subtreeRootElement.appendChild(textNode);
|
||||
}
|
||||
} else if (key.equals(CDATA_NODE_TAG)) {
|
||||
if (childEntry.getValue() instanceof List) {
|
||||
buildCDataList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
|
||||
} else {
|
||||
final Node cDataNode = doc.createCDATASection(childEntry.getValue().toString());
|
||||
subtreeRootElement.appendChild(cDataNode);
|
||||
}
|
||||
} else if (key.equals(COMMENT_NODE_TAG)) {
|
||||
if (childEntry.getValue() instanceof List) {
|
||||
buildCommentList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
|
||||
} else {
|
||||
final Node commentNode = doc.createComment(childEntry.getValue().toString());
|
||||
subtreeRootElement.appendChild(commentNode);
|
||||
}
|
||||
} //else if (key.equals(WHITESPACE_NODE_TAG)
|
||||
// || key.equals(SIGNIFICANT_WHITESPACE_NODE_TAG)) {
|
||||
// Ignore
|
||||
//} else {
|
||||
/*
|
||||
* All other nodes whose name starts with a '#' are invalid XML
|
||||
* nodes, and thus ignored:
|
||||
*/
|
||||
//}
|
||||
} else {
|
||||
final Element newElement = doc.createElement(key);
|
||||
subtreeRootElement.appendChild(newElement);
|
||||
buildXmlFromHashMap(doc, newElement, childEntry.getValue());
|
||||
}
|
||||
}
|
||||
} else if (elementContent instanceof List) {
|
||||
buildXmlFromJsonArray(doc, subtreeRootElement, (List<Object>) elementContent);
|
||||
} else {
|
||||
if (elementContent != null) {
|
||||
subtreeRootElement.setTextContent(elementContent.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildTextNodeList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final String nodeValue : nodeValues) {
|
||||
sb.append(nodeValue);
|
||||
}
|
||||
subtreeRoot.appendChild(doc.createTextNode(sb.toString()));
|
||||
}
|
||||
|
||||
private static void buildCDataList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
|
||||
for (final String nodeValue : nodeValues) {
|
||||
subtreeRoot.appendChild(doc.createCDATASection(nodeValue));
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildCommentList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
|
||||
for (final String nodeValue : nodeValues) {
|
||||
subtreeRoot.appendChild(doc.createComment(nodeValue));
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildXmlFromJsonArray(
|
||||
final Document doc,
|
||||
final Node listHeadNode,
|
||||
final List<Object> elementContentList) {
|
||||
final Node subtreeRootNode = listHeadNode.getParentNode();
|
||||
/* The head node (already exists) */
|
||||
buildXmlFromHashMap(doc, (Element) listHeadNode, elementContentList.get(0));
|
||||
/* The rest of the list */
|
||||
for (final Object elementContent : elementContentList.subList(1, elementContentList.size())) {
|
||||
final Element newElement = doc.createElement(listHeadNode.getNodeName());
|
||||
subtreeRootNode.appendChild(newElement);
|
||||
buildXmlFromHashMap(doc, newElement, elementContent);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Element> deserializeCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeCollectionCustom(Reader);
|
||||
}
|
||||
|
||||
public static void deserializeCollection(final JsonReader reader, final Collection<Element> res) throws IOException {
|
||||
reader.deserializeCollection(Reader, res);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ArrayList<Element> deserializeNullableCollection(final JsonReader reader) throws IOException {
|
||||
return reader.deserializeNullableCollectionCustom(Reader);
|
||||
}
|
||||
|
||||
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Element> res) throws IOException {
|
||||
reader.deserializeNullableCollection(Reader, res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of FairEmail.
|
||||
|
||||
FairEmail 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.
|
||||
|
||||
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2024 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AI {
|
||||
private static final int MAX_SUMMARIZE_TEXT_SIZE = 10 * 1024;
|
||||
|
||||
static boolean isAvailable(Context context) {
|
||||
return (OpenAI.isAvailable(context) || Gemini.isAvailable(context));
|
||||
}
|
||||
|
||||
static String completeChat(Context context, long id, CharSequence body) throws JSONException, IOException {
|
||||
if (body == null || body.length() == 0)
|
||||
return null;
|
||||
|
||||
if (OpenAI.isAvailable(context)) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String model = prefs.getString("openai_model", OpenAI.DEFAULT_MODEL);
|
||||
float temperature = prefs.getFloat("openai_temperature", OpenAI.DEFAULT_TEMPERATURE);
|
||||
boolean multimodal = prefs.getBoolean("openai_multimodal", true);
|
||||
|
||||
OpenAI.Message message;
|
||||
if (body instanceof Spannable)
|
||||
message = new OpenAI.Message(OpenAI.USER,
|
||||
OpenAI.Content.get((Spannable) body, id, multimodal, context));
|
||||
else
|
||||
message = new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{
|
||||
new OpenAI.Content(OpenAI.CONTENT_TEXT, body.toString())});
|
||||
|
||||
OpenAI.Message[] completions =
|
||||
OpenAI.completeChat(context, model, new OpenAI.Message[]{message}, temperature, 1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (OpenAI.Message completion : completions)
|
||||
for (OpenAI.Content content : completion.getContent())
|
||||
if (OpenAI.CONTENT_TEXT.equals(content.getType())) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(content.getContent()
|
||||
.replaceAll("^\\n+", "")
|
||||
.replaceAll("\\n+$", ""));
|
||||
}
|
||||
return sb.toString();
|
||||
} else if (Gemini.isAvailable(context)) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String model = prefs.getString("gemini_model", Gemini.DEFAULT_MODEL);
|
||||
float temperature = prefs.getFloat("gemini_temperature", Gemini.DEFAULT_TEMPERATURE);
|
||||
|
||||
Gemini.Message message = new Gemini.Message(Gemini.USER,
|
||||
new String[]{Gemini.truncateParagraphs(body.toString())});
|
||||
Gemini.Message[] completions = Gemini.generate(context, model, new Gemini.Message[]{message}, temperature, 1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Gemini.Message completion : completions)
|
||||
for (String result : completion.getContent()) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(result
|
||||
.replaceAll("^\\n+", "")
|
||||
.replaceAll("\\n+$", ""));
|
||||
}
|
||||
return sb.toString();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
static String getSummarizePrompt(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (OpenAI.isAvailable(context))
|
||||
return prefs.getString("openai_summarize", OpenAI.DEFAULT_SUMMARY_PROMPT);
|
||||
else if (Gemini.isAvailable(context))
|
||||
return prefs.getString("gemini_summarize", Gemini.DEFAULT_SUMMARY_PROMPT);
|
||||
else
|
||||
return context.getString(R.string.title_summarize);
|
||||
}
|
||||
|
||||
static String getSummaryText(Context context, EntityMessage message) throws JSONException, IOException {
|
||||
File file = message.getFile(context);
|
||||
if (!file.exists())
|
||||
return null;
|
||||
|
||||
Document d = JsoupEx.parse(file);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean remove_signatures = prefs.getBoolean("remove_signatures", false);
|
||||
if (remove_signatures)
|
||||
HtmlHelper.removeSignatures(d);
|
||||
|
||||
HtmlHelper.removeQuotes(d);
|
||||
|
||||
d = HtmlHelper.sanitizeView(context, d, false);
|
||||
|
||||
HtmlHelper.truncate(d, MAX_SUMMARIZE_TEXT_SIZE);
|
||||
|
||||
if (OpenAI.isAvailable(context)) {
|
||||
String model = prefs.getString("openai_model", OpenAI.DEFAULT_MODEL);
|
||||
float temperature = prefs.getFloat("openai_temperature", OpenAI.DEFAULT_TEMPERATURE);
|
||||
String prompt = prefs.getString("openai_summarize", OpenAI.DEFAULT_SUMMARY_PROMPT);
|
||||
boolean multimodal = prefs.getBoolean("openai_multimodal", true);
|
||||
|
||||
List<OpenAI.Message> input = new ArrayList<>();
|
||||
input.add(new OpenAI.Message(OpenAI.USER,
|
||||
new OpenAI.Content[]{new OpenAI.Content(OpenAI.CONTENT_TEXT, prompt)}));
|
||||
|
||||
if (!TextUtils.isEmpty(message.subject))
|
||||
input.add(new OpenAI.Message(OpenAI.USER,
|
||||
new OpenAI.Content[]{new OpenAI.Content(OpenAI.CONTENT_TEXT, message.subject)}));
|
||||
|
||||
SpannableStringBuilder ssb = HtmlHelper.fromDocument(context, d, null, null);
|
||||
input.add(new OpenAI.Message(OpenAI.USER,
|
||||
OpenAI.Content.get(ssb, message.id, multimodal, context)));
|
||||
|
||||
OpenAI.Message[] completions =
|
||||
OpenAI.completeChat(context, model, input.toArray(new OpenAI.Message[0]), temperature, 1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (OpenAI.Message completion : completions)
|
||||
for (OpenAI.Content content : completion.getContent())
|
||||
if (OpenAI.CONTENT_TEXT.equals(content.getType())) {
|
||||
if (sb.length() != 0)
|
||||
sb.append('\n');
|
||||
sb.append(content.getContent());
|
||||
}
|
||||
return sb.toString();
|
||||
} else if (Gemini.isAvailable(context)) {
|
||||
String model = prefs.getString("gemini_model", Gemini.DEFAULT_MODEL);
|
||||
float temperature = prefs.getFloat("gemini_temperature", Gemini.DEFAULT_TEMPERATURE);
|
||||
String prompt = prefs.getString("gemini_summarize", Gemini.DEFAULT_SUMMARY_PROMPT);
|
||||
|
||||
String text = d.text();
|
||||
if (TextUtils.isEmpty(text))
|
||||
return null;
|
||||
Gemini.Message content = new Gemini.Message(Gemini.USER, new String[]{prompt, text});
|
||||
|
||||
Gemini.Message[] completions =
|
||||
Gemini.generate(context, model, new Gemini.Message[]{content}, temperature, 1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Gemini.Message completion : completions)
|
||||
for (String result : completion.getContent()) {
|
||||
if (sb.length() != 0)
|
||||
sb.append('\n');
|
||||
sb.append(result);
|
||||
}
|
||||
return sb.toString();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -69,11 +69,11 @@ public class ActivityAMP extends ActivityBase {
|
|||
if (savedInstanceState != null)
|
||||
force_light = savedInstanceState.getBoolean("fair:force_light");
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.activity_amp, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
wvAmp = findViewById(R.id.wvAmp);
|
||||
pbWait = findViewById(R.id.pbWait);
|
||||
grpReady = findViewById(R.id.grpReady);
|
||||
|
|
|
@ -54,11 +54,11 @@ public class ActivityAnswer extends ActivityBase {
|
|||
final CharSequence query = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
|
||||
final boolean readonly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false);
|
||||
|
||||
getSupportActionBar().setSubtitle(query == null ? null : query.toString());
|
||||
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.activity_answer, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setSubtitle(query == null ? null : query.toString());
|
||||
|
||||
ListView lvAnswer = view.findViewById(R.id.lvAnswer);
|
||||
Group grpReady = view.findViewById(R.id.grpReady);
|
||||
ContentLoadingProgressBar pbWait = view.findViewById(R.id.pbWait);
|
||||
|
|
|
@ -20,8 +20,6 @@ package eu.faircode.email;
|
|||
*/
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
|
@ -40,19 +38,26 @@ import android.os.PowerManager;
|
|||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
@ -63,6 +68,8 @@ import androidx.lifecycle.LifecycleOwner;
|
|||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -92,6 +99,131 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
return originalContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean hide_toolbar = prefs.getBoolean("hide_toolbar", !BuildConfig.PLAY_STORE_RELEASE);
|
||||
boolean edge_to_edge = prefs.getBoolean("edge_to_edge", false);
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(this);
|
||||
ViewGroup holder = (ViewGroup) inflater.inflate(R.layout.toolbar_holder, null);
|
||||
if (BuildConfig.DEBUG)
|
||||
holder.setBackgroundColor(Color.RED);
|
||||
|
||||
AppBarLayout appbar = holder.findViewById(R.id.appbar);
|
||||
Toolbar toolbar = holder.findViewById(R.id.toolbar);
|
||||
View placeholder = holder.findViewById(R.id.placeholder);
|
||||
|
||||
toolbar.setPopupTheme(getThemeId());
|
||||
if (hide_toolbar) {
|
||||
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
|
||||
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL |
|
||||
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
|
||||
toolbar.setLayoutParams(params);
|
||||
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
|
||||
@Override
|
||||
public void onBackStackChanged() {
|
||||
try {
|
||||
appbar.setExpanded(true);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
holder.removeView(placeholder);
|
||||
holder.addView(view, placeholder.getLayoutParams());
|
||||
|
||||
int abh = Helper.getActionBarHeight(this);
|
||||
appbar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
|
||||
@Override
|
||||
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
|
||||
try {
|
||||
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
mlp.topMargin = abh + offset;
|
||||
view.setLayoutParams(mlp);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FragmentDialogTheme.setBackground(this, holder, this instanceof ActivityCompose);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(holder, (v, windowInsets) -> {
|
||||
try {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
mlp.leftMargin = insets.left;
|
||||
mlp.topMargin = insets.top;
|
||||
mlp.rightMargin = insets.right;
|
||||
if (!edge_to_edge)
|
||||
mlp.bottomMargin = insets.bottom;
|
||||
v.setLayoutParams(mlp);
|
||||
|
||||
if (edge_to_edge)
|
||||
for (View child : Helper.getViewsWithTag(v, "inset")) {
|
||||
mlp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
|
||||
mlp.bottomMargin = insets.bottom;
|
||||
child.setLayoutParams(mlp);
|
||||
}
|
||||
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
if (this instanceof ActivityCompose)
|
||||
ViewCompat.setWindowInsetsAnimationCallback(
|
||||
holder,
|
||||
new WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
|
||||
@NonNull
|
||||
@Override
|
||||
public WindowInsetsCompat onProgress(
|
||||
@NonNull WindowInsetsCompat windowInsets,
|
||||
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
|
||||
try {
|
||||
// https://developer.android.com/develop/ui/views/layout/sw-keyboard
|
||||
for (WindowInsetsAnimationCompat animation : runningAnimations)
|
||||
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
|
||||
int pad = bottom - insets.bottom;
|
||||
holder.setPaddingRelative(0, 0, 0, pad < 0 ? 0 : pad);
|
||||
break;
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
return windowInsets;
|
||||
}
|
||||
});
|
||||
|
||||
super.setContentView(holder);
|
||||
|
||||
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
|
||||
view.post(new RunnableEx("setBackgroundColor") {
|
||||
@Override
|
||||
public void delegate() {
|
||||
getWindow().getDecorView().setBackgroundColor(colorPrimaryDark);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(int layoutResID) {
|
||||
View view = LayoutInflater.from(this).inflate(layoutResID, null);
|
||||
setContentView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
EntityLog.log(this, "Activity create " + this.getClass().getName() +
|
||||
|
@ -104,6 +236,8 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
|
||||
getSupportFragmentManager().registerFragmentLifecycleCallbacks(lifecycleCallbacks, true);
|
||||
|
||||
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
|
||||
|
||||
this.contacts = hasPermission(Manifest.permission.READ_CONTACTS);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
@ -116,15 +250,9 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
themeId = FragmentDialogTheme.getTheme(this);
|
||||
setTheme(themeId);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
boolean dark = Helper.isDarkTheme(this);
|
||||
Window window = getWindow();
|
||||
View view = window.getDecorView();
|
||||
int flags = view.getSystemUiVisibility();
|
||||
if (dark)
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
view.setSystemUiVisibility(flags);
|
||||
}
|
||||
EdgeToEdge.enable(this);
|
||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView())
|
||||
.setAppearanceLightStatusBars(false);
|
||||
}
|
||||
|
||||
String requestKey = getRequestKey();
|
||||
|
@ -153,8 +281,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
|
||||
|
||||
try {
|
||||
Drawable d = getDrawable(R.drawable.baseline_mail_24);
|
||||
Bitmap bm = Bitmap.createBitmap(
|
||||
|
@ -184,13 +310,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
Log.e(ex);
|
||||
}
|
||||
|
||||
boolean navbar_colorize = prefs.getBoolean("navbar_colorize", false);
|
||||
if (navbar_colorize) {
|
||||
Window window = getWindow();
|
||||
if (window != null)
|
||||
window.setNavigationBarColor(colorPrimaryDark);
|
||||
}
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
|
||||
Fragment bfragment = fm.findFragmentByTag("androidx.biometric.BiometricFragment");
|
||||
|
@ -900,80 +1019,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
|
|||
return super.shouldUpRecreateTask(targetIntent);
|
||||
}
|
||||
|
||||
public boolean abShowing = true;
|
||||
public ValueAnimator abAnimator = null;
|
||||
|
||||
public boolean isActionBarShown() {
|
||||
return abShowing;
|
||||
}
|
||||
|
||||
public void showActionBar(boolean show) {
|
||||
ViewGroup abv = findViewById(androidx.appcompat.R.id.action_bar);
|
||||
if (abv == null)
|
||||
return;
|
||||
|
||||
if (abShowing == show)
|
||||
return;
|
||||
abShowing = show;
|
||||
|
||||
int height = Helper.getActionBarHeight(this);
|
||||
int current = abv.getLayoutParams().height;
|
||||
int target = (show ? height : 0);
|
||||
Log.i("ActionBar height=" + current + "..." + target);
|
||||
|
||||
|
||||
if (abAnimator != null)
|
||||
abAnimator.cancel();
|
||||
|
||||
abAnimator = ValueAnimator.ofInt(current, target);
|
||||
|
||||
abAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator anim) {
|
||||
try {
|
||||
Integer v = (Integer) anim.getAnimatedValue();
|
||||
Log.i("ActionBar height=" + v);
|
||||
ViewGroup.LayoutParams lparam = abv.getLayoutParams();
|
||||
if (lparam.height == v)
|
||||
Log.i("ActionBar ---");
|
||||
else {
|
||||
lparam.height = v;
|
||||
abv.requestLayout();
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
abAnimator.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(@NonNull Animator animation) {
|
||||
Log.i("ActionBar start");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(@NonNull Animator animation) {
|
||||
Log.i("ActionBar end");
|
||||
abAnimator = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(@NonNull Animator animation) {
|
||||
Log.i("ActionBar cancel");
|
||||
abAnimator = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(@NonNull Animator animation) {
|
||||
Log.i("ActionBar repeat");
|
||||
}
|
||||
});
|
||||
|
||||
abAnimator.setDuration(ACTIONBAR_ANIMATION_DURATION * Math.abs(current - target) / height);
|
||||
abAnimator.start();
|
||||
}
|
||||
|
||||
Handler getMainHandler() {
|
||||
return ApplicationEx.getMainHandler();
|
||||
}
|
||||
|
|
|
@ -35,9 +35,10 @@ public class ActivityClear extends ActivityBase {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_clear);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getString(R.string.title_advanced_clear_all));
|
||||
setContentView(R.layout.activity_clear);
|
||||
|
||||
btnClearAll = findViewById(R.id.btnClearAll);
|
||||
btnCancel = findViewById(R.id.btnCancel);
|
||||
|
|
|
@ -90,8 +90,6 @@ public class ActivityCode extends ActivityBase {
|
|||
searching = savedInstanceState.getString("fair:searching");
|
||||
}
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
|
@ -102,6 +100,8 @@ public class ActivityCode extends ActivityBase {
|
|||
View view = LayoutInflater.from(this).inflate(R.layout.activity_code, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
wvCode = findViewById(R.id.wvCode);
|
||||
pbWait = findViewById(R.id.pbWait);
|
||||
grpReady = findViewById(R.id.grpReady);
|
||||
|
|
|
@ -56,8 +56,6 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
|
|||
setContentView(R.layout.activity_compose);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setCustomView(R.layout.action_bar);
|
||||
getSupportActionBar().setDisplayShowCustomEnabled(true);
|
||||
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
|
||||
|
|
|
@ -64,18 +64,17 @@ public class ActivityDMARC extends ActivityBase {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(R.string.title_advanced_dmarc_viewer);
|
||||
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.activity_dmarc, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(R.string.title_advanced_dmarc_viewer);
|
||||
|
||||
tvDmarc = findViewById(R.id.tvDmarc);
|
||||
pbWait = findViewById(R.id.pbWait);
|
||||
grpReady = findViewById(R.id.grpReady);
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(this, view, false);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
|
||||
load();
|
||||
|
|
|
@ -46,18 +46,17 @@ public class ActivityDSN extends ActivityBase {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle("DSN");
|
||||
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.activity_dsn, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle("DSN");
|
||||
|
||||
tvHeaders = findViewById(R.id.tvHeaders);
|
||||
pbWait = findViewById(R.id.pbWait);
|
||||
grpReady = findViewById(R.id.grpReady);
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(this, view, false);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
|
||||
load();
|
||||
|
|
|
@ -109,12 +109,12 @@ public class ActivityEML extends ActivityBase {
|
|||
junk = savedInstanceState.getBoolean("fair:junk");
|
||||
}
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle("EML");
|
||||
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.activity_eml, null);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle("EML");
|
||||
|
||||
tvFrom = findViewById(R.id.tvFrom);
|
||||
tvTo = findViewById(R.id.tvTo);
|
||||
tvReplyTo = findViewById(R.id.tvReplyTo);
|
||||
|
@ -207,7 +207,6 @@ public class ActivityEML extends ActivityBase {
|
|||
});
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(this, view, false);
|
||||
vSeparatorAttachments.setVisibility(View.GONE);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
cardHeaders.setVisibility(View.GONE);
|
||||
|
|
|
@ -51,9 +51,10 @@ public class ActivityError extends ActivityBase {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_error);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getString(R.string.title_setup_error));
|
||||
setContentView(R.layout.activity_error);
|
||||
|
||||
tvTitle = findViewById(R.id.tvTitle);
|
||||
tvMessage = findViewById(R.id.tvMessage);
|
||||
|
|
|
@ -120,10 +120,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
|
|||
view = LayoutInflater.from(this).inflate(R.layout.activity_setup, null);
|
||||
setContentView(view);
|
||||
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setCustomView(R.layout.action_bar);
|
||||
getSupportActionBar().setDisplayShowCustomEnabled(true);
|
||||
|
||||
drawerLayout = findViewById(R.id.drawer_layout);
|
||||
drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim));
|
||||
|
|
|
@ -88,13 +88,13 @@ public class ActivitySignature extends ActivityBase {
|
|||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean monospaced = prefs.getBoolean("monospaced", false);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature));
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(this);
|
||||
view = (ViewGroup) inflater.inflate(R.layout.activity_signature, null, false);
|
||||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature));
|
||||
|
||||
tvHtmlRemark = findViewById(R.id.tvHtmlRemark);
|
||||
etText = findViewById(R.id.etText);
|
||||
ibFull = findViewById(R.id.ibFull);
|
||||
|
@ -208,7 +208,6 @@ public class ActivitySignature extends ActivityBase {
|
|||
});
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(this, view, true);
|
||||
tvHtmlRemark.setVisibility(View.GONE);
|
||||
style_bar.setVisibility(View.GONE);
|
||||
|
||||
|
|
|
@ -218,8 +218,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
iff.addAction(ACTION_NEW_MESSAGE);
|
||||
lbm.registerReceiver(creceiver, iff);
|
||||
|
||||
if (savedInstanceState != null)
|
||||
if (savedInstanceState != null) {
|
||||
initialized = savedInstanceState.getBoolean("fair:initialized");
|
||||
searching = savedInstanceState.getBoolean("fair:searching");
|
||||
}
|
||||
|
||||
colorDrawerScrim = Helper.resolveColor(this, R.attr.colorDrawerScrim);
|
||||
|
||||
|
@ -267,8 +269,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
setContentView(view);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setCustomView(R.layout.action_bar);
|
||||
getSupportActionBar().setDisplayShowCustomEnabled(true);
|
||||
|
||||
content_separator = findViewById(R.id.content_separator);
|
||||
content_pane = findViewById(R.id.content_pane);
|
||||
|
@ -745,12 +745,15 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
boolean unified = (intent != null && "unified".equals(intent.getAction()));
|
||||
if (!search && !(standalone && !unified))
|
||||
init();
|
||||
else
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
if (savedInstanceState != null)
|
||||
drawerToggle.setDrawerIndicatorEnabled(savedInstanceState.getBoolean("fair:toggle"));
|
||||
|
||||
checkFirst();
|
||||
if (!"inbox".equals(startup))
|
||||
checkFirst();
|
||||
checkBanner();
|
||||
checkCrash();
|
||||
|
||||
|
@ -804,6 +807,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
fragment.setArguments(args);
|
||||
setFragment(fragment);
|
||||
checkIntent();
|
||||
checkFirst();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
|
@ -850,6 +854,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putParcelable("fair:intent", getIntent());
|
||||
outState.putBoolean("fair:toggle", drawerToggle == null || drawerToggle.isDrawerIndicatorEnabled());
|
||||
outState.putBoolean("fair:initialized", initialized);
|
||||
outState.putBoolean("fair:searching", searching);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
@ -1386,8 +1391,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
if (count == 0)
|
||||
finish();
|
||||
else {
|
||||
showActionBar(true);
|
||||
|
||||
if (count < lastBackStackCount) {
|
||||
Intent intent = getIntent();
|
||||
intent.setAction(null);
|
||||
|
@ -1525,6 +1528,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
|
||||
String last = prefs.getString("changelog", null);
|
||||
if (!Objects.equals(version, last) || BuildConfig.DEBUG) {
|
||||
prefs.edit().putString("changelog", version).apply();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString("name", "CHANGELOG.md");
|
||||
FragmentDialogMarkdown fragment = new FragmentDialogMarkdown();
|
||||
|
@ -1532,8 +1537,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
fragment.show(getSupportFragmentManager(), "changelog");
|
||||
}
|
||||
}
|
||||
|
||||
prefs.edit().putString("changelog", version).apply();
|
||||
}
|
||||
|
||||
private void checkBanner() {
|
||||
|
|
|
@ -109,9 +109,10 @@ public class ActivityWidget extends ActivityBase {
|
|||
|
||||
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
|
||||
|
||||
setContentView(R.layout.activity_widget);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(R.string.title_widget_title_count);
|
||||
setContentView(R.layout.activity_widget);
|
||||
|
||||
spAccount = findViewById(R.id.spAccount);
|
||||
spFolder = findViewById(R.id.spFolder);
|
||||
|
|
|
@ -66,9 +66,10 @@ public class ActivityWidgetSync extends ActivityBase {
|
|||
|
||||
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
|
||||
|
||||
setContentView(R.layout.activity_widget_sync);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(R.string.title_widget_title_sync);
|
||||
setContentView(R.layout.activity_widget_sync);
|
||||
|
||||
cbDayNight = findViewById(R.id.cbDayNight);
|
||||
cbSemiTransparent = findViewById(R.id.cbSemiTransparent);
|
||||
|
|
|
@ -127,9 +127,10 @@ public class ActivityWidgetUnified extends ActivityBase {
|
|||
|
||||
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
|
||||
|
||||
setContentView(R.layout.activity_widget_unified);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setSubtitle(R.string.title_widget_title_list);
|
||||
setContentView(R.layout.activity_widget_unified);
|
||||
|
||||
spAccount = findViewById(R.id.spAccount);
|
||||
spFolder = findViewById(R.id.spFolder);
|
||||
|
|
|
@ -708,11 +708,11 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
submenu.add(Menu.FIRST, R.string.title_notify_batch_disable, 5, R.string.title_notify_batch_disable);
|
||||
submenu.add(Menu.FIRST, R.string.title_unified_inbox_add, 6, R.string.title_unified_inbox_add);
|
||||
submenu.add(Menu.FIRST, R.string.title_unified_inbox_delete, 7, R.string.title_unified_inbox_delete);
|
||||
submenu.add(Menu.FIRST, R.string.title_navigation_folder, 6, R.string.title_navigation_folder);
|
||||
submenu.add(Menu.FIRST, R.string.title_navigation_folder_hide, 7, R.string.title_navigation_folder_hide);
|
||||
submenu.add(Menu.FIRST, R.string.title_synchronize_more, 8, R.string.title_synchronize_more);
|
||||
submenu.add(Menu.FIRST, R.string.title_download_batch_enable, 9, R.string.title_download_batch_enable);
|
||||
submenu.add(Menu.FIRST, R.string.title_download_batch_disable, 10, R.string.title_download_batch_disable);
|
||||
submenu.add(Menu.FIRST, R.string.title_navigation_folder, 8, R.string.title_navigation_folder);
|
||||
submenu.add(Menu.FIRST, R.string.title_navigation_folder_hide, 9, R.string.title_navigation_folder_hide);
|
||||
submenu.add(Menu.FIRST, R.string.title_synchronize_more, 10, R.string.title_synchronize_more);
|
||||
submenu.add(Menu.FIRST, R.string.title_download_batch_enable, 11, R.string.title_download_batch_enable);
|
||||
submenu.add(Menu.FIRST, R.string.title_download_batch_disable, 12, R.string.title_download_batch_disable);
|
||||
}
|
||||
|
||||
if (folder.account != null && folder.accountProtocol == EntityAccount.TYPE_IMAP)
|
||||
|
@ -869,11 +869,10 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
EntityOperation.sync(context, folder.id, true, !children);
|
||||
|
||||
if (children) {
|
||||
List<EntityFolder> folders = db.folder().getChildFolders(folder.id);
|
||||
if (folders != null)
|
||||
for (EntityFolder child : folders)
|
||||
if (child.selectable)
|
||||
EntityOperation.sync(context, child.id, true);
|
||||
List<EntityFolder> folders = EntityFolder.getChildFolders(context, folder.id);
|
||||
for (EntityFolder child : folders)
|
||||
if (child.selectable)
|
||||
EntityOperation.sync(context, child.id, true);
|
||||
}
|
||||
|
||||
if (folder.account != null) {
|
||||
|
@ -935,10 +934,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
return null;
|
||||
|
||||
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
|
||||
for (EntityFolder child : children)
|
||||
db.folder().setFolderSynchronize(child.id, enabled);
|
||||
|
||||
|
@ -973,10 +970,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
return null;
|
||||
|
||||
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
|
||||
for (EntityFolder child : children)
|
||||
db.folder().setFolderNotify(child.id, enabled);
|
||||
|
||||
|
@ -1009,10 +1004,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
return null;
|
||||
|
||||
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
|
||||
for (EntityFolder child : children)
|
||||
db.folder().setFolderUnified(child.id, add);
|
||||
|
||||
|
@ -1045,10 +1038,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
return null;
|
||||
|
||||
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
|
||||
for (EntityFolder child : children)
|
||||
db.folder().setFolderNavigation(child.id, enabled);
|
||||
|
||||
|
@ -1092,10 +1083,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
|
|||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
return null;
|
||||
|
||||
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
|
||||
for (EntityFolder child : children)
|
||||
db.folder().setFolderDownload(child.id, enabled);
|
||||
|
||||
|
|
|
@ -229,6 +229,8 @@ public class AdapterKeyword extends RecyclerView.Adapter<AdapterKeyword.ViewHold
|
|||
else
|
||||
prefs.edit().putInt(key, keyword.color).apply();
|
||||
|
||||
prefs.edit().remove("keyword." + keyword.name);
|
||||
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.sendBroadcast(new Intent(FragmentMessages.ACTION_KEYWORDS));
|
||||
}
|
||||
|
|
|
@ -179,6 +179,7 @@ import java.util.Properties;
|
|||
import java.util.SortedMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import javax.mail.Address;
|
||||
import javax.mail.Session;
|
||||
|
@ -478,6 +479,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private ImageButton ibSearchText;
|
||||
private ImageButton ibSearch;
|
||||
private ImageButton ibTranslate;
|
||||
private ImageButton ibSummarize;
|
||||
private ImageButton ibFullScreen;
|
||||
private ImageButton ibForceLight;
|
||||
private ImageButton ibImportance;
|
||||
|
@ -927,6 +929,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearchText = vsBody.findViewById(R.id.ibSearchText);
|
||||
ibSearch = vsBody.findViewById(R.id.ibSearch);
|
||||
ibTranslate = vsBody.findViewById(R.id.ibTranslate);
|
||||
ibSummarize = vsBody.findViewById(R.id.ibSummarize);
|
||||
ibFullScreen = vsBody.findViewById(R.id.ibFullScreen);
|
||||
ibForceLight = vsBody.findViewById(R.id.ibForceLight);
|
||||
ibImportance = vsBody.findViewById(R.id.ibImportance);
|
||||
|
@ -1099,6 +1102,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearch.setOnClickListener(this);
|
||||
ibTranslate.setOnClickListener(this);
|
||||
ibTranslate.setOnLongClickListener(this);
|
||||
ibSummarize.setOnClickListener(this);
|
||||
ibFullScreen.setOnClickListener(this);
|
||||
ibForceLight.setOnClickListener(this);
|
||||
ibImportance.setOnClickListener(this);
|
||||
|
@ -1221,6 +1225,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearch.setOnClickListener(null);
|
||||
ibTranslate.setOnClickListener(null);
|
||||
ibTranslate.setOnLongClickListener(null);
|
||||
ibSummarize.setOnClickListener(null);
|
||||
ibFullScreen.setOnClickListener(null);
|
||||
ibForceLight.setOnClickListener(null);
|
||||
ibImportance.setOnClickListener(null);
|
||||
|
@ -1839,6 +1844,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearchText.setVisibility(View.GONE);
|
||||
ibSearch.setVisibility(View.GONE);
|
||||
ibTranslate.setVisibility(View.GONE);
|
||||
ibSummarize.setVisibility(View.GONE);
|
||||
ibFullScreen.setVisibility(View.GONE);
|
||||
ibForceLight.setVisibility(View.GONE);
|
||||
ibImportance.setVisibility(View.GONE);
|
||||
|
@ -2135,6 +2141,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearchText.setVisibility(View.GONE);
|
||||
ibSearch.setVisibility(View.GONE);
|
||||
ibTranslate.setVisibility(View.GONE);
|
||||
ibSummarize.setVisibility(View.GONE);
|
||||
ibFullScreen.setVisibility(View.GONE);
|
||||
ibForceLight.setVisibility(View.GONE);
|
||||
ibImportance.setVisibility(View.GONE);
|
||||
|
@ -2349,6 +2356,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
boolean button_hide = prefs.getBoolean("button_hide", false);
|
||||
boolean button_importance = prefs.getBoolean("button_importance", false);
|
||||
boolean button_translate = prefs.getBoolean("button_translate", true);
|
||||
boolean button_summarize = prefs.getBoolean("button_summarize", false);
|
||||
boolean button_full_screen = prefs.getBoolean("button_full_screen", false);
|
||||
boolean button_force_light = prefs.getBoolean("button_force_light", true);
|
||||
boolean button_search = prefs.getBoolean("button_search", false);
|
||||
|
@ -2389,6 +2397,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibSearchText.setVisibility(tools && !outbox && button_search_text && message.content ? View.VISIBLE : View.GONE);
|
||||
ibSearch.setVisibility(tools && !outbox && button_search && (froms > 0 || tos > 0) ? View.VISIBLE : View.GONE);
|
||||
ibTranslate.setVisibility(tools && !outbox && button_translate && DeepL.isAvailable(context) && message.content ? View.VISIBLE : View.GONE);
|
||||
ibSummarize.setVisibility(tools && !outbox && button_summarize && AI.isAvailable(context) && message.content ? View.VISIBLE : View.GONE);
|
||||
ibFullScreen.setVisibility(tools && full && button_full_screen && message.content ? View.VISIBLE : View.GONE);
|
||||
ibForceLight.setVisibility(tools && full && dark && button_force_light && message.content ? View.VISIBLE : View.GONE);
|
||||
ibForceLight.setImageLevel(!(canDarken || fake_dark) || force_light ? 1 : 0);
|
||||
|
@ -2546,7 +2555,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
} else {
|
||||
boolean homoPersonal = TextHelper.isSingleScript(personal);
|
||||
if (BuildConfig.DEBUG && !homoPersonal)
|
||||
if (debug && !homoPersonal)
|
||||
personal = TextHelper.getNonLatinCodepoints(personal);
|
||||
ssb.append(personal);
|
||||
if (!homoPersonal) {
|
||||
|
@ -2554,6 +2563,13 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ssb.setSpan(new StyleSpan(Typeface.BOLD), start, ssb.length(), 0);
|
||||
ssb.setSpan(new ForegroundColorSpan(colorError), start, ssb.length(), 0);
|
||||
}
|
||||
|
||||
Matcher m = Helper.EMAIL_ADDRESS.matcher(personal);
|
||||
while (m.find()) {
|
||||
ssb.setSpan(new StyleSpan(Typeface.BOLD), m.start(), m.end(), 0);
|
||||
ssb.setSpan(new ForegroundColorSpan(colorError), m.start(), m.end(), 0);
|
||||
}
|
||||
|
||||
if (full) {
|
||||
ssb.append(" <");
|
||||
if (!TextUtils.isEmpty(email)) {
|
||||
|
@ -4440,7 +4456,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
else if (id == R.id.ibVerified)
|
||||
onShowVerified(message);
|
||||
else if (id == R.id.ibAuth)
|
||||
onShowAuth(message);
|
||||
onShowAuth(message, null);
|
||||
else if (id == R.id.ibPriority)
|
||||
onShowPriority(message);
|
||||
else if (id == R.id.ibSensitivity)
|
||||
|
@ -4529,6 +4545,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
onSearchContact(message, false);
|
||||
} else if (id == R.id.ibTranslate) {
|
||||
onActionTranslate(message);
|
||||
} else if (id == R.id.ibSummarize) {
|
||||
onActionSummarize(message);
|
||||
} else if (id == R.id.ibFullScreen)
|
||||
onActionOpenFull(message);
|
||||
else if (id == R.id.ibForceLight) {
|
||||
|
@ -4892,12 +4910,15 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
|
||||
private void onShowVerified(TupleMessageEx message) {
|
||||
ToastEx.makeText(context, ibVerified.getContentDescription(), Toast.LENGTH_LONG).show();
|
||||
onShowAuth(message, ibVerified.getContentDescription().toString());
|
||||
}
|
||||
|
||||
private void onShowAuth(TupleMessageEx message) {
|
||||
private void onShowAuth(TupleMessageEx message, String title) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (title != null)
|
||||
sb.append(title).append('\n');
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
if (Boolean.FALSE.equals(message.dkim))
|
||||
result.add("DKIM");
|
||||
|
@ -4911,61 +4932,48 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
result.add("MX");
|
||||
|
||||
if (result.size() > 0)
|
||||
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result)));
|
||||
else {
|
||||
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result)))
|
||||
.append('\n');
|
||||
|
||||
if (authentication_indicator) {
|
||||
if (check_tls)
|
||||
sb.append("TLS: ")
|
||||
.append(message.tls == null ? "-" : (message.tls ? "✓" : "✗"))
|
||||
.append('\n');
|
||||
.append(message.tls == null ? "-" : (message.tls ? "✓" : "✗")).append('\n');
|
||||
sb.append("DKIM: ")
|
||||
.append(message.dkim == null ? "-" : (message.dkim ? "✓" : "✗"))
|
||||
.append('\n');
|
||||
.append(message.dkim == null ? "-" : (message.dkim ? "✓" : "✗")).append('\n');
|
||||
sb.append("SPF: ")
|
||||
.append(message.spf == null ? "-" : (message.spf ? "✓" : "✗"))
|
||||
.append('\n');
|
||||
.append(message.spf == null ? "-" : (message.spf ? "✓" : "✗")).append('\n');
|
||||
sb.append("DMARC: ")
|
||||
.append(message.dmarc == null ? "-" : (message.dmarc ? "✓" : "✗"))
|
||||
.append('\n');
|
||||
.append(message.dmarc == null ? "-" : (message.dmarc ? "✓" : "✗")).append('\n');
|
||||
if (message.auth != null)
|
||||
sb.append("SMTP: ").append(message.auth ? "✓" : "✗");
|
||||
sb.append("SMTP: ")
|
||||
.append(message.auth ? "✓" : "✗").append('\n');
|
||||
if (check_mx)
|
||||
sb.append('\n')
|
||||
.append("MX: ")
|
||||
.append(message.mx == null ? "-" : (message.mx ? "✓" : "✗"));
|
||||
sb.append("MX: ")
|
||||
.append(message.mx == null ? "-" : (message.mx ? "✓" : "✗")).append('\n');
|
||||
}
|
||||
|
||||
if (native_dkim && !TextUtils.isEmpty(message.signedby)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append("Signed by:");
|
||||
sb.append(context.getString(R.string.title_signed_by)).append(' ');
|
||||
for (String signer : message.signedby.split(","))
|
||||
sb.append('\n').append(signer);
|
||||
sb.append(signer).append('\n');
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(message.blocklist)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(context.getString(R.string.title_on_blocklist));
|
||||
}
|
||||
if (Boolean.TRUE.equals(message.blocklist))
|
||||
sb.append(context.getString(R.string.title_on_blocklist)).append('\n');
|
||||
|
||||
if (Boolean.FALSE.equals(message.from_domain) && message.smtp_from != null)
|
||||
for (Address smtp_from : message.smtp_from) {
|
||||
String domain = UriHelper.getEmailDomain(((InternetAddress) smtp_from).getAddress());
|
||||
String root = UriHelper.getRootDomain(context, domain);
|
||||
if (root != null) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(context.getString(R.string.title_via, root));
|
||||
}
|
||||
if (root != null)
|
||||
sb.append(context.getString(R.string.title_via, root)).append('\n');
|
||||
}
|
||||
|
||||
if (Boolean.FALSE.equals(message.reply_domain)) {
|
||||
String[] warning = message.checkReplyDomain(context);
|
||||
if (warning != null) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(context.getString(R.string.title_reply_domain, warning[0], warning[1]));
|
||||
}
|
||||
if (warning != null)
|
||||
sb.append(context.getString(R.string.title_reply_domain, warning[0], warning[1])).append('\n');
|
||||
}
|
||||
|
||||
if (message.from != null && message.from.length > 0) {
|
||||
|
@ -4975,6 +4983,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
sb.insert(0, '\n').insert(0, domain);
|
||||
}
|
||||
|
||||
if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n')
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
|
||||
ToastEx.makeText(context, sb.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
|
@ -5436,7 +5447,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
for (Address from : message.from) {
|
||||
if (!(from instanceof InternetAddress))
|
||||
continue;
|
||||
if (!TextHelper.isSingleScript(((InternetAddress) from).getPersonal()))
|
||||
String personal = ((InternetAddress) from).getPersonal();
|
||||
if (TextUtils.isEmpty(personal))
|
||||
continue;
|
||||
if (!TextHelper.isSingleScript(personal))
|
||||
return true;
|
||||
if (Helper.EMAIL_ADDRESS.matcher(personal).find())
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -6185,7 +6201,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
boolean experiments = prefs.getBoolean("experiments", false);
|
||||
|
||||
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, ibMore);
|
||||
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, ibMore == null ? view : ibMore);
|
||||
popupMenu.inflate(R.menu.popup_message_more);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_unseen)
|
||||
|
@ -6239,6 +6255,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
popupMenu.getMenu().findItem(R.id.menu_search_in_text).setEnabled(message.content && !full);
|
||||
popupMenu.getMenu().findItem(R.id.menu_translate).setVisible(
|
||||
DeepL.isAvailable(context) && message.content);
|
||||
popupMenu.getMenu().findItem(R.id.menu_summarize).setVisible(
|
||||
AI.isAvailable(context) && message.content);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_force_light).setVisible(full && dark);
|
||||
popupMenu.getMenu().findItem(R.id.menu_force_light).setChecked(force_light);
|
||||
|
@ -6348,6 +6366,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
} else if (itemId == R.id.menu_translate) {
|
||||
onActionTranslate(message);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_summarize) {
|
||||
onActionSummarize(message);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_force_light) {
|
||||
onActionForceLight(message);
|
||||
return true;
|
||||
|
@ -7256,6 +7277,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
}
|
||||
|
||||
private void onActionSummarize(TupleMessageEx message) {
|
||||
FragmentDialogSummarize.summarize(message, parentFragment.getParentFragmentManager());
|
||||
}
|
||||
|
||||
private void onActionForceLight(TupleMessageEx message) {
|
||||
if (canDarken || fake_dark) {
|
||||
boolean force_light = !properties.getValue("force_light", message.id);
|
||||
|
@ -7761,7 +7786,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
return tvBody.getText().subSequence(start, end);
|
||||
}
|
||||
|
||||
private View.AccessibilityDelegate accessibilityDelegateHeader = new View.AccessibilityDelegate() {
|
||||
private final View.AccessibilityDelegate accessibilityDelegateHeader = new View.AccessibilityDelegate() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
|
@ -7798,63 +7823,82 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
Log.e(ex);
|
||||
}
|
||||
|
||||
TupleMessageEx message = getMessage();
|
||||
if (message == null)
|
||||
return;
|
||||
try {
|
||||
TupleMessageEx message = getMessage();
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
boolean expanded = properties.getValue("expanded", message.id);
|
||||
boolean expanded = properties.getValue("expanded", message.id);
|
||||
|
||||
vwColor.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
vwColor.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
if (ibExpander.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibExpander,
|
||||
context.getString(expanded ? R.string.title_accessibility_collapse : R.string.title_accessibility_expand)));
|
||||
ibExpander.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
if (ibExpander.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibExpander,
|
||||
context.getString(expanded ? R.string.title_accessibility_collapse : R.string.title_accessibility_expand)));
|
||||
ibExpander.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSeen,
|
||||
context.getString(message.ui_seen ? R.string.title_unseen : R.string.title_seen)));
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSeen,
|
||||
context.getString(message.ui_seen ? R.string.title_unseen : R.string.title_seen)));
|
||||
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAnswer,
|
||||
context.getString(R.string.title_reply)));
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAnswer,
|
||||
context.getString(R.string.title_reply)));
|
||||
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibArchive,
|
||||
context.getString(R.string.title_archive)));
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibArchive,
|
||||
context.getString(R.string.title_archive)));
|
||||
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibTrash,
|
||||
context.getString(R.string.title_trash)));
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibTrash,
|
||||
context.getString(R.string.title_trash)));
|
||||
|
||||
if (properties.getSelectionCount() > 0)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibDelete,
|
||||
context.getString(R.string.title_trash_selection)));
|
||||
if (properties.getSelectionCount() > 0)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibDelete,
|
||||
context.getString(R.string.title_trash_selection)));
|
||||
|
||||
if (ibAvatar.getVisibility() == View.VISIBLE && ibAvatar.isEnabled())
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAvatar,
|
||||
context.getString(R.string.title_accessibility_view_contact)));
|
||||
ibAvatar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
if (ibAvatar != null) {
|
||||
if (ibAvatar.getVisibility() == View.VISIBLE && ibAvatar.isEnabled())
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAvatar,
|
||||
context.getString(R.string.title_accessibility_view_contact)));
|
||||
ibAvatar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
if (ibFlagged.getVisibility() == View.VISIBLE && ibFlagged.isEnabled()) {
|
||||
int flagged = (message.count - message.unflagged);
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibFlagged,
|
||||
context.getString(flagged > 0 ? R.string.title_unflag : R.string.title_flag)));
|
||||
if (ibFlagged != null) {
|
||||
if (ibFlagged.getVisibility() == View.VISIBLE && ibFlagged.isEnabled()) {
|
||||
int flagged = (message.count - message.unflagged);
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibFlagged,
|
||||
context.getString(flagged > 0 ? R.string.title_unflag : R.string.title_flag)));
|
||||
}
|
||||
ibFlagged.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
if (ibAuth != null) {
|
||||
if (ibAuth.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAuth,
|
||||
context.getString(R.string.title_accessibility_show_authentication_result)));
|
||||
ibAuth.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
if (ibSnoozed != null) {
|
||||
if (ibSnoozed.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSnoozed,
|
||||
context.getString(R.string.title_accessibility_show_snooze_time)));
|
||||
ibSnoozed.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibMore,
|
||||
context.getString(R.string.title_advanced_more)));
|
||||
if (ibMore != null)
|
||||
ibMore.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
if (ibError != null) {
|
||||
if (ibError.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibError,
|
||||
context.getString(R.string.title_accessibility_view_help)));
|
||||
ibError.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
info.setContentDescription(populateContentDescription(message));
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
ibFlagged.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
if (ibAuth.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAuth,
|
||||
context.getString(R.string.title_accessibility_show_authentication_result)));
|
||||
ibAuth.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
if (ibSnoozed.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSnoozed,
|
||||
context.getString(R.string.title_accessibility_show_snooze_time)));
|
||||
ibSnoozed.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
if (ibError.getVisibility() == View.VISIBLE)
|
||||
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibError,
|
||||
context.getString(R.string.title_accessibility_view_help)));
|
||||
ibError.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
info.setContentDescription(populateContentDescription(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7888,11 +7932,14 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
onToggleFlag(message);
|
||||
return true;
|
||||
} else if (action == R.id.ibAuth) {
|
||||
onShowAuth(message);
|
||||
onShowAuth(message, null);
|
||||
return true;
|
||||
} else if (action == R.id.ibSnoozed) {
|
||||
onShowSnoozed(message);
|
||||
return true;
|
||||
} else if (action == R.id.ibMore) {
|
||||
onActionMore(message);
|
||||
return true;
|
||||
} else if (action == R.id.ibError) {
|
||||
onHelp(message);
|
||||
return true;
|
||||
|
@ -8481,6 +8528,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
same = false;
|
||||
log("last_attempt changed " + prev.last_attempt + "/" + next.last_attempt, next.id);
|
||||
}
|
||||
// last_touched
|
||||
|
||||
// accountPop
|
||||
if (!Objects.equals(prev.accountName, next.accountName)) {
|
||||
|
@ -8720,12 +8768,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
if (message != null) {
|
||||
keyPosition.put(message.id, i);
|
||||
positionKey.put(i, message.id);
|
||||
|
||||
addExtra(message.from, message.extra);
|
||||
|
||||
if (threading) {
|
||||
if (message.senders == null || message.senders.length == 0)
|
||||
message.senders = message.from;
|
||||
if (message.recipients == null || message.recipients.length == 0)
|
||||
message.recipients = message.to;
|
||||
message.senders = merge(message.from, message.senders);
|
||||
message.recipients = merge(message.to, message.recipients);
|
||||
addExtra(message.senders, message.extra);
|
||||
} else {
|
||||
message.senders = message.from;
|
||||
|
@ -8753,6 +8801,28 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
});
|
||||
}
|
||||
|
||||
static Address[] merge(Address[] base, Address[] addresses) {
|
||||
if (base == null || base.length == 0)
|
||||
return (addresses == null ? new Address[0] : addresses);
|
||||
if (addresses == null)
|
||||
return base;
|
||||
|
||||
List<Address> result = new ArrayList<>();
|
||||
result.addAll(Arrays.asList(base));
|
||||
for (Address a : addresses)
|
||||
for (Address b : base) {
|
||||
if (a.equals(b)) {
|
||||
if (a instanceof InternetAddress && b instanceof InternetAddress) {
|
||||
if (Objects.equals(((InternetAddress) a).getPersonal(), ((InternetAddress) b).getPersonal()))
|
||||
continue;
|
||||
} else
|
||||
continue;
|
||||
}
|
||||
result.add(a);
|
||||
}
|
||||
return result.toArray(new Address[0]);
|
||||
}
|
||||
|
||||
static void addExtra(Address[] addresses, String extra) {
|
||||
if (addresses == null || addresses.length == 0)
|
||||
return;
|
||||
|
|
|
@ -163,9 +163,17 @@ public class AdapterNavUnified extends RecyclerView.Adapter<AdapterNavUnified.Vi
|
|||
if (folder == null)
|
||||
return;
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String startup = prefs.getString("startup", "unified");
|
||||
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
if (EntityFolder.OUTBOX.equals(folder.type))
|
||||
lbm.sendBroadcast(new Intent(ActivityView.ACTION_VIEW_OUTBOX));
|
||||
else if ("inbox".equals(startup) && EntityFolder.INBOX.equals(folder.type))
|
||||
lbm.sendBroadcast(
|
||||
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
|
||||
.putExtra("type", (String) null)
|
||||
.putExtra("unified", true));
|
||||
else if (folder.folders > 1 || folder.type == null)
|
||||
lbm.sendBroadcast(
|
||||
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
|
||||
|
|
|
@ -143,25 +143,30 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
|
|||
JSONObject jcondition = new JSONObject(rule.condition);
|
||||
if (jcondition.has("sender"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_sender),
|
||||
jcondition.getJSONObject("sender").optBoolean("not"),
|
||||
jcondition.getJSONObject("sender").optString("value"),
|
||||
jcondition.getJSONObject("sender").optBoolean("regex")));
|
||||
if (jcondition.has("recipient"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_recipient),
|
||||
jcondition.getJSONObject("recipient").optBoolean("not"),
|
||||
jcondition.getJSONObject("recipient").optString("value"),
|
||||
jcondition.getJSONObject("recipient").optBoolean("regex")));
|
||||
if (jcondition.has("subject"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_subject),
|
||||
jcondition.getJSONObject("subject").optBoolean("not"),
|
||||
jcondition.getJSONObject("subject").optString("value"),
|
||||
jcondition.getJSONObject("subject").optBoolean("regex")));
|
||||
if (jcondition.optBoolean("attachments"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_attachments),
|
||||
null, null));
|
||||
false, null, null));
|
||||
if (jcondition.has("header"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_header),
|
||||
jcondition.getJSONObject("header").optBoolean("not"),
|
||||
jcondition.getJSONObject("header").optString("value"),
|
||||
jcondition.getJSONObject("header").optBoolean("regex")));
|
||||
if (jcondition.has("body"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_body),
|
||||
jcondition.getJSONObject("body").optBoolean("not"),
|
||||
jcondition.getJSONObject("body").optString("value"),
|
||||
jcondition.getJSONObject("body").optBoolean("regex")));
|
||||
if (jcondition.has("date")) {
|
||||
|
@ -173,7 +178,7 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
|
|||
range = DF.format(after) + " - " + DF.format(before);
|
||||
}
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_time_abs),
|
||||
range, null));
|
||||
false, range, null));
|
||||
}
|
||||
if (jcondition.has("schedule")) {
|
||||
String range = null;
|
||||
|
@ -185,18 +190,20 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
|
|||
Helper.formatHour(context, end % (24 * 60));
|
||||
}
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_time_rel),
|
||||
range, null));
|
||||
false, range, null));
|
||||
}
|
||||
|
||||
if (jcondition.has("expression"))
|
||||
conditions.add(new Condition(context.getString(R.string.title_rule_expression),
|
||||
jcondition.getString("expression"), null));
|
||||
false, jcondition.getString("expression"), null));
|
||||
|
||||
SpannableStringBuilder ssb = new SpannableStringBuilderEx();
|
||||
for (Condition condition : conditions) {
|
||||
if (ssb.length() > 0)
|
||||
ssb.append("\n");
|
||||
ssb.append(condition.name);
|
||||
if (condition.not)
|
||||
ssb.append(' ').append(context.getString(R.string.title_rule_not));
|
||||
if (!TextUtils.isEmpty(condition.condition)) {
|
||||
ssb.append(" \"");
|
||||
int start = ssb.length();
|
||||
|
@ -609,6 +616,8 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
|
|||
return R.string.title_rule_url;
|
||||
case EntityRule.TYPE_SILENT:
|
||||
return R.string.title_rule_silent;
|
||||
case EntityRule.TYPE_SUMMARIZE:
|
||||
return R.string.title_rule_summarize;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown action type=" + type);
|
||||
}
|
||||
|
@ -616,11 +625,13 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
|
|||
|
||||
private class Condition {
|
||||
private final String name;
|
||||
private boolean not;
|
||||
private final String condition;
|
||||
private final Boolean regex;
|
||||
|
||||
Condition(String name, String condition, Boolean regex) {
|
||||
Condition(String name, boolean not, String condition, Boolean regex) {
|
||||
this.name = name;
|
||||
this.not = not;
|
||||
this.condition = condition;
|
||||
this.regex = regex;
|
||||
}
|
||||
|
|
|
@ -856,6 +856,9 @@ public class ApplicationEx extends Application
|
|||
if (Build.PRODUCT == null || !Build.PRODUCT.endsWith("_beta") ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
editor.putBoolean("mod", false);
|
||||
} else if (version < 2180) {
|
||||
if (Helper.isAndroid15())
|
||||
editor.putInt("last_sdk", 0);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !BuildConfig.DEBUG)
|
||||
|
@ -865,6 +868,11 @@ public class ApplicationEx extends Application
|
|||
editor.putInt("previous_version", version);
|
||||
editor.putInt("version", BuildConfig.VERSION_CODE);
|
||||
|
||||
int last_sdk = prefs.getInt("last_sdk", Build.VERSION.SDK_INT);
|
||||
if (Helper.isAndroid15() && last_sdk <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
editor.remove("setup_reminder");
|
||||
editor.putInt("last_sdk", Build.VERSION.SDK_INT);
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
|
|
@ -358,6 +358,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
criteria.with_size,
|
||||
criteria.after,
|
||||
criteria.before,
|
||||
criteria.touched == null ? null : new Date().getTime() - criteria.touched * 3600 * 1000L,
|
||||
SEARCH_LIMIT_DEVICE, state.offset);
|
||||
EntityLog.log(context, "Boundary device" +
|
||||
" account=" + account +
|
||||
|
@ -805,15 +806,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
if (criteria.after != null) {
|
||||
if (message.received < criteria.after)
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
if (criteria.before != null) {
|
||||
if (message.received > criteria.before)
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
if (criteria.after != null) {
|
||||
if (message.received < criteria.after)
|
||||
if (criteria.touched != null) {
|
||||
if (message.last_attempt == null || message.last_attempt < new Date().getTime() - criteria.touched * 3600 * 1000L)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -879,7 +885,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
if (criteria.in_message) {
|
||||
// This won't match <p>An <b>example</b><p> when searching for "An example"
|
||||
if (contains(html, criteria.query, partial, true)) {
|
||||
String text = HtmlHelper.getFullText(html);
|
||||
String text = HtmlHelper.getFullText(html, false);
|
||||
if (contains(text, criteria.query, partial, false))
|
||||
return true;
|
||||
}
|
||||
|
@ -1042,6 +1048,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
boolean in_junk = true;
|
||||
Long after = null;
|
||||
Long before = null;
|
||||
Integer touched = null;
|
||||
|
||||
private static final String FROM = "from:";
|
||||
private static final String TO = "to:";
|
||||
|
@ -1295,6 +1302,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
if (with_size != null)
|
||||
flags.add(context.getString(R.string.title_search_flag_size,
|
||||
Helper.humanReadableByteCount(with_size)));
|
||||
if (touched != null)
|
||||
flags.add(context.getString(R.string.title_search_flag_touched));
|
||||
return (query == null ? "" : query + " ")
|
||||
+ (flags.size() > 0 ? "+" : "")
|
||||
+ TextUtils.join(",", flags);
|
||||
|
@ -1326,7 +1335,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
this.in_trash == other.in_trash &&
|
||||
this.in_junk == other.in_junk &&
|
||||
Objects.equals(this.after, other.after) &&
|
||||
Objects.equals(this.before, other.before));
|
||||
Objects.equals(this.before, other.before) &&
|
||||
Objects.equals(this.touched, other.touched));
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
@ -1376,12 +1386,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
if (before != null)
|
||||
json.put("before", before - now.getTimeInMillis());
|
||||
|
||||
if (touched != null)
|
||||
json.put("touched", touched);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public static SearchCriteria fromJsonData(JSONObject json) throws JSONException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.query = json.optString("query");
|
||||
if (!json.isNull("query"))
|
||||
criteria.query = json.optString("query");
|
||||
criteria.fts = json.optBoolean("fts");
|
||||
criteria.in_senders = json.optBoolean("in_senders");
|
||||
criteria.in_recipients = json.optBoolean("in_recipients");
|
||||
|
@ -1424,6 +1438,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
if (json.has("before"))
|
||||
criteria.before = json.getLong("before") + now.getTimeInMillis();
|
||||
|
||||
if (json.has("touched"))
|
||||
criteria.touched = json.getInt("touched");
|
||||
|
||||
return criteria;
|
||||
}
|
||||
|
||||
|
@ -1452,7 +1469,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
" trash=" + in_trash +
|
||||
" junk=" + in_junk +
|
||||
" after=" + (after == null ? "" : new Date(after)) +
|
||||
" before=" + (before == null ? "" : new Date(before));
|
||||
" before=" + (before == null ? "" : new Date(before)) +
|
||||
" touched=" + touched;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,7 +115,6 @@ public class ConnectionHelper {
|
|||
"NO", // Norway
|
||||
"PL", // Poland
|
||||
"PT", // Portugal
|
||||
"RE", // La Réunion
|
||||
"RO", // Romania
|
||||
"SK", // Slovakia
|
||||
"SI", // Slovenia
|
||||
|
|
|
@ -64,6 +64,7 @@ import com.sun.mail.pop3.POP3Store;
|
|||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
|
@ -204,8 +205,10 @@ class Core {
|
|||
@Override
|
||||
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
|
||||
long ago = System.currentTimeMillis() - protocol.getTimestamp();
|
||||
if (ago > 20000)
|
||||
if (ago > 20000) {
|
||||
Log.i("NOOP ago=" + ago + " ms");
|
||||
protocol.noop();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
@ -2074,7 +2077,7 @@ class Core {
|
|||
String body = parts.getHtml(context, plain_text, charset);
|
||||
File file = message.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
message.preview = HtmlHelper.getPreview(text);
|
||||
message.language = HtmlHelper.getLanguage(context, message.subject, text);
|
||||
Integer plain_only = parts.isPlainOnly();
|
||||
|
@ -2247,7 +2250,7 @@ class Core {
|
|||
String body = parts.getHtml(context, download_plain);
|
||||
File file = message.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
message.preview = HtmlHelper.getPreview(text);
|
||||
message.language = HtmlHelper.getLanguage(context, message.subject, text);
|
||||
|
||||
|
@ -3134,6 +3137,7 @@ class Core {
|
|||
if (!message.content)
|
||||
throw new IllegalArgumentException("Message without content id=" + rule.id + ":" + rule.name);
|
||||
|
||||
rule.async = true;
|
||||
rule.execute(context, message, null);
|
||||
}
|
||||
|
||||
|
@ -3609,7 +3613,7 @@ class Core {
|
|||
|
||||
File file = message.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
message.preview = HtmlHelper.getPreview(text);
|
||||
message.language = HtmlHelper.getLanguage(context, message.subject, text);
|
||||
db.message().setMessageContent(message.id,
|
||||
|
@ -4896,7 +4900,7 @@ class Core {
|
|||
body = parts.getHtml(context, download_plain);
|
||||
File file = message.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
message.content = true;
|
||||
message.preview = HtmlHelper.getPreview(text);
|
||||
message.language = HtmlHelper.getLanguage(context, message.subject, text);
|
||||
|
@ -5416,7 +5420,7 @@ class Core {
|
|||
String body = parts.getHtml(context);
|
||||
File file = message.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
message.preview = HtmlHelper.getPreview(text);
|
||||
message.language = HtmlHelper.getLanguage(context, message.subject, text);
|
||||
db.message().setMessageContent(message.id,
|
||||
|
|
|
@ -30,7 +30,9 @@ import org.json.JSONException;
|
|||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -68,7 +70,7 @@ import javax.mail.internet.InternetAddress;
|
|||
// https://developer.android.com/topic/libraries/architecture/room.html
|
||||
|
||||
@Database(
|
||||
version = 291,
|
||||
version = 293,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
|
@ -123,6 +125,7 @@ public abstract class DB extends RoomDatabase {
|
|||
static final String DB_NAME = "fairemail";
|
||||
static final int DEFAULT_QUERY_THREADS = 4; // AndroidX default thread count: 4
|
||||
static final int DEFAULT_CACHE_SIZE = 20; // percentage of memory class
|
||||
private static final long DB_LOCK_TIMEOUT = 60 * 1000L;
|
||||
private static final int DB_JOURNAL_SIZE_LIMIT = 1048576; // requery/sqlite-android default
|
||||
private static final int DB_CHECKPOINT = 1000; // requery/sqlite-android default
|
||||
|
||||
|
@ -148,7 +151,7 @@ public abstract class DB extends RoomDatabase {
|
|||
File dbfile = configuration.context.getDatabasePath(DB_NAME);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(configuration.context);
|
||||
boolean sqlite_integrity_check = prefs.getBoolean("sqlite_integrity_check", true);
|
||||
boolean sqlite_integrity_check = prefs.getBoolean("sqlite_integrity_check", false);
|
||||
|
||||
// https://www.sqlite.org/pragma.html#pragma_integrity_check
|
||||
if (sqlite_integrity_check && dbfile.exists()) {
|
||||
|
@ -416,6 +419,8 @@ public abstract class DB extends RoomDatabase {
|
|||
|
||||
Log.i("Disabled view invalidation");
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
// Should never happen
|
||||
Log.forceCrashReporting();
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
|
@ -425,6 +430,42 @@ public abstract class DB extends RoomDatabase {
|
|||
Log.d("ROOM invalidated=" + TextUtils.join(",", tables));
|
||||
}
|
||||
});
|
||||
|
||||
// Ref: https://android-review.googlesource.com/c/platform/frameworks/support/+/1797472
|
||||
Log.i("DB critical section start");
|
||||
File dbDir = context.getDatabasePath(DB_NAME).getParentFile();
|
||||
dbDir.mkdirs();
|
||||
File lockFile = new File(dbDir, DB_NAME + ".lock");
|
||||
try (FileOutputStream fos = new FileOutputStream(lockFile)) {
|
||||
ObjectHolder<FileLock> lock = new ObjectHolder<>(null);
|
||||
try {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
lock.value = fos.getChannel().lock();
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
thread.join(DB_LOCK_TIMEOUT);
|
||||
if (thread.isAlive())
|
||||
throw new IllegalArgumentException("DB critical section failed");
|
||||
|
||||
// Force migration
|
||||
sInstance.getOpenHelper().getWritableDatabase();
|
||||
} finally {
|
||||
if (lock.value != null)
|
||||
lock.value.release();
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
// Should never happen
|
||||
Log.forceCrashReporting();
|
||||
Log.e(ex);
|
||||
}
|
||||
Log.i("DB critical section end");
|
||||
}
|
||||
|
||||
return sInstance;
|
||||
|
@ -547,6 +588,7 @@ public abstract class DB extends RoomDatabase {
|
|||
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.kt:104)
|
||||
at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:706)
|
||||
*/
|
||||
// Should never happen
|
||||
Log.forceCrashReporting();
|
||||
Log.e(ex);
|
||||
// FrameworkSQLiteOpenHelper.innerGetDatabase will delete the database
|
||||
|
@ -2950,6 +2992,18 @@ public abstract class DB extends RoomDatabase {
|
|||
logMigration(startVersion, endVersion);
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `last_view` INTEGER");
|
||||
}
|
||||
}).addMigrations(new Migration(291, 292) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
logMigration(startVersion, endVersion);
|
||||
db.execSQL("ALTER TABLE `identity` ADD COLUMN `envelopeFrom` TEXT");
|
||||
}
|
||||
}).addMigrations(new Migration(292, 293) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
logMigration(startVersion, endVersion);
|
||||
db.execSQL("ALTER TABLE `message` ADD COLUMN `last_touched` INTEGER");
|
||||
}
|
||||
}).addMigrations(new Migration(998, 999) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
|
|
|
@ -32,8 +32,8 @@ public interface DaoLog {
|
|||
" WHERE time > :from" +
|
||||
" AND (:type IS NULL OR type = :type)" +
|
||||
" ORDER BY time DESC" +
|
||||
" LIMIT 2000")
|
||||
LiveData<List<EntityLog>> liveLogs(long from, Integer type);
|
||||
" LIMIT :limit")
|
||||
LiveData<List<EntityLog>> liveLogs(long from, int limit, Integer type);
|
||||
|
||||
@Query("SELECT * FROM log" +
|
||||
" WHERE time > :from" +
|
||||
|
|
|
@ -32,8 +32,6 @@ import androidx.room.Update;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import javax.mail.Address;
|
||||
|
||||
@Dao
|
||||
public interface DaoMessage {
|
||||
|
||||
|
@ -77,15 +75,7 @@ public interface DaoMessage {
|
|||
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
|
||||
" OR (NOT :found AND folder.type = :type)" +
|
||||
" THEN message.received ELSE 0 END) AS dummy" +
|
||||
" FROM (SELECT * FROM message" +
|
||||
" WHERE message.thread IN" +
|
||||
" (SELECT DISTINCT mm.thread FROM folder ff" +
|
||||
" JOIN message mm ON mm.folder = ff.id" +
|
||||
" WHERE ((:found AND mm.ui_found)" +
|
||||
" OR (NOT :found AND :type IS NULL AND ff.unified)" +
|
||||
" OR (NOT :found AND :type IS NOT NULL AND ff.type = :type))" +
|
||||
" AND (NOT mm.ui_hide OR :debug))" +
|
||||
" ORDER BY received DESC) AS message" + // group_concat
|
||||
" FROM message" +
|
||||
" JOIN account_view AS account ON account.id = message.account" +
|
||||
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
|
||||
" JOIN folder_view AS folder ON folder.id = message.folder" +
|
||||
|
@ -97,6 +87,7 @@ public interface DaoMessage {
|
|||
" HAVING (SUM((:found AND message.ui_found)" +
|
||||
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
|
||||
" OR (NOT :found AND :type IS NOT NULL AND folder.type = :type)) > 0)" + // thread can be the same in different accounts
|
||||
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
|
||||
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0)" +
|
||||
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0)" +
|
||||
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0)" +
|
||||
|
@ -114,6 +105,7 @@ public interface DaoMessage {
|
|||
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
|
||||
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
|
||||
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
|
||||
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE" +
|
||||
|
@ -160,13 +152,7 @@ public interface DaoMessage {
|
|||
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
|
||||
" OR (NOT :found AND folder.id = :folder)" +
|
||||
" THEN message.received ELSE 0 END) AS dummy" +
|
||||
" FROM (SELECT * FROM message" +
|
||||
" WHERE message.thread IN" +
|
||||
" (SELECT DISTINCT mm.thread FROM message mm" +
|
||||
" WHERE mm.folder = :folder" +
|
||||
" AND (NOT mm.ui_hide OR :debug)" +
|
||||
" AND (NOT :found OR mm.ui_found))" +
|
||||
" ORDER BY received DESC) AS message" + // group_concat
|
||||
" FROM message" +
|
||||
" JOIN account_view AS account ON account.id = message.account" +
|
||||
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
|
||||
" JOIN folder_view AS folder ON folder.id = message.folder" +
|
||||
|
@ -176,7 +162,10 @@ public interface DaoMessage {
|
|||
" AND (NOT message.ui_hide OR :debug)" +
|
||||
" AND (NOT :found OR message.ui_found = :found)" +
|
||||
" GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
|
||||
" HAVING (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
|
||||
" HAVING (SUM((:found AND message.ui_found)" +
|
||||
" OR (NOT :found AND message.folder = :folder)) > 0)" +
|
||||
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
|
||||
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
|
||||
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0 OR " + is_outbox + ")" +
|
||||
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0" +
|
||||
" OR " + is_outbox + " OR " + is_drafts + " OR " + is_sent + ")" +
|
||||
|
@ -193,6 +182,7 @@ public interface DaoMessage {
|
|||
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
|
||||
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
|
||||
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
|
||||
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE" +
|
||||
|
@ -345,6 +335,12 @@ public interface DaoMessage {
|
|||
" ORDER BY message.received DESC")
|
||||
List<Long> getMessageIdsByFolder(Long folder);
|
||||
|
||||
@Query("SELECT identity, COUNT(*) AS count" +
|
||||
" FROM message" +
|
||||
" WHERE folder = :folder" +
|
||||
" GROUP BY identity")
|
||||
List<TupleIdentityCount> getIdentitiesByFolder(long folder);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT message.id FROM message" +
|
||||
" JOIN folder_view AS folder ON folder.id = message.folder" +
|
||||
|
@ -383,9 +379,10 @@ public interface DaoMessage {
|
|||
" AND (:size IS NULL OR total > :size)" +
|
||||
" AND (:after IS NULL OR received > :after)" +
|
||||
" AND (:before IS NULL OR received < :before)" +
|
||||
" AND (:touched IS NULL OR last_touched > :touched)" +
|
||||
" AND NOT message.folder IN (:exclude)" +
|
||||
" GROUP BY message.id" +
|
||||
" ORDER BY received DESC" +
|
||||
" ORDER BY CASE WHEN :touched IS NULL THEN received ELSE last_touched END DESC" +
|
||||
" LIMIT :limit OFFSET :offset")
|
||||
List<TupleMatch> matchMessages(
|
||||
Long account, Long folder, long[] exclude, String find,
|
||||
|
@ -393,7 +390,7 @@ public interface DaoMessage {
|
|||
boolean unseen, boolean flagged, boolean hidden, boolean encrypted, boolean with_attachments, boolean with_notes,
|
||||
int type_count, String[] types,
|
||||
Integer size,
|
||||
Long after, Long before,
|
||||
Long after, Long before, Long touched,
|
||||
int limit, int offset);
|
||||
|
||||
@Query("SELECT id" +
|
||||
|
@ -926,7 +923,10 @@ public interface DaoMessage {
|
|||
int setMessageVerified(long id, boolean verified);
|
||||
|
||||
@Query("UPDATE message SET last_attempt = :last_attempt WHERE id = :id AND NOT (last_attempt IS :last_attempt)")
|
||||
int setMessageLastAttempt(long id, long last_attempt);
|
||||
int setMessageLastAttempt(long id, Long last_attempt);
|
||||
|
||||
@Query("UPDATE message SET last_touched = :last_touched WHERE id = :id AND NOT (last_touched IS :last_touched)")
|
||||
int setMessageLastTouched(long id, Long last_touched);
|
||||
|
||||
@Query("UPDATE message SET ui_ignored = 1" +
|
||||
" WHERE NOT ui_ignored" +
|
||||
|
@ -1068,4 +1068,172 @@ public interface DaoMessage {
|
|||
" ORDER BY received DESC" +
|
||||
" LIMIT :keep)")
|
||||
int deleteMessagesKeep(long folder, int keep);
|
||||
|
||||
@Transaction
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
@Query("SELECT message.*" +
|
||||
", account.pop AS accountProtocol, account.name AS accountName, account.category AS accountCategory, COALESCE(identity.color, folder.color, account.color) AS accountColor" +
|
||||
", account.notify AS accountNotify, account.summary AS accountSummary, account.leave_deleted AS accountLeaveDeleted, account.auto_seen AS accountAutoSeen" +
|
||||
", folder.name AS folderName, folder.color AS folderColor, folder.display AS folderDisplay, folder.type AS folderType, NULL AS folderInheritedType, folder.unified AS folderUnified, folder.read_only AS folderReadOnly" +
|
||||
", IFNULL(identity.display, identity.name) AS identityName, identity.email AS identityEmail, identity.color AS identityColor, identity.synchronize AS identitySynchronize" +
|
||||
", '[' || substr(group_concat(message.`from`, ','), 0, 2048) || ']' AS senders" +
|
||||
", '[' || substr(group_concat(message.`to`, ','), 0, 2048) || ']' AS recipients" +
|
||||
", COUNT(message.id) AS count" +
|
||||
", SUM(1 - message.ui_seen) AS unseen" +
|
||||
", SUM(1 - message.ui_flagged) AS unflagged" +
|
||||
", SUM(folder.type = '" + EntityFolder.DRAFTS + "') AS drafts" +
|
||||
", COUNT(DISTINCT" +
|
||||
" CASE WHEN NOT message.hash IS NULL THEN message.hash" +
|
||||
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
|
||||
" ELSE message.id END) AS visible" +
|
||||
", COUNT(DISTINCT" +
|
||||
" CASE WHEN message.ui_seen THEN NULL" +
|
||||
" WHEN NOT message.hash IS NULL THEN message.hash" +
|
||||
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
|
||||
" ELSE message.id END) AS visible_unseen" +
|
||||
", SUM(message.attachments) AS totalAttachments" +
|
||||
", SUM(message.total) AS totalSize" +
|
||||
", message.priority AS ui_priority" +
|
||||
", message.importance AS ui_importance" +
|
||||
", MAX(CASE WHEN" +
|
||||
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
|
||||
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
|
||||
" OR (NOT :found AND folder.type = :type)" +
|
||||
" THEN message.received ELSE 0 END) AS dummy" +
|
||||
" FROM (SELECT * FROM message" +
|
||||
" WHERE message.thread IN" +
|
||||
" (SELECT DISTINCT mm.thread FROM folder ff" +
|
||||
" JOIN message mm ON mm.folder = ff.id" +
|
||||
" WHERE ((:found AND mm.ui_found)" +
|
||||
" OR (NOT :found AND :type IS NULL AND ff.unified)" +
|
||||
" OR (NOT :found AND :type IS NOT NULL AND ff.type = :type))" +
|
||||
" AND (NOT mm.ui_hide OR :debug))" +
|
||||
" ORDER BY received DESC) AS message" + // group_concat
|
||||
" JOIN account_view AS account ON account.id = message.account" +
|
||||
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
|
||||
" JOIN folder_view AS folder ON folder.id = message.folder" +
|
||||
" WHERE account.`synchronize`" +
|
||||
" AND (:threading OR (:type IS NULL AND (folder.unified OR :found)) OR (:type IS NOT NULL AND folder.type = :type))" +
|
||||
" AND (NOT message.ui_hide OR :debug)" +
|
||||
" AND (NOT :found OR message.ui_found = :found)" +
|
||||
" GROUP BY account.id, CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
|
||||
" HAVING (SUM((:found AND message.ui_found)" +
|
||||
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
|
||||
" OR (NOT :found AND :type IS NOT NULL AND folder.type = :type)) > 0)" + // thread can be the same in different accounts
|
||||
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
|
||||
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0)" +
|
||||
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0)" +
|
||||
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0)" +
|
||||
" AND (NOT :filter_snoozed OR message.ui_snoozed IS NULL OR " + is_drafts + ")" +
|
||||
" AND (NOT :filter_deleted OR NOT message.ui_deleted)" +
|
||||
" AND (:filter_language IS NULL OR SUM(message.language = :filter_language) > 0)" +
|
||||
" ORDER BY CASE WHEN :found THEN 0 ELSE -IFNULL(message.importance, 1) END" +
|
||||
", CASE WHEN :group_category THEN account.category ELSE '' END COLLATE NOCASE" +
|
||||
", CASE" +
|
||||
" WHEN 'unread' = :sort1 THEN SUM(1 - message.ui_seen) = 0" +
|
||||
" WHEN 'starred' = :sort1 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
|
||||
" WHEN 'priority' = :sort1 THEN -IFNULL(message.priority, 1)" +
|
||||
" WHEN 'sender' = :sort1 THEN LOWER(message.sender)" +
|
||||
" WHEN 'subject' = :sort1 THEN LOWER(message.subject)" +
|
||||
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
|
||||
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
|
||||
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
|
||||
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE" +
|
||||
" WHEN 'unread' = :sort2 THEN SUM(1 - message.ui_seen) = 0" +
|
||||
" WHEN 'starred' = :sort2 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE WHEN :ascending THEN message.received ELSE -message.received END")
|
||||
DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedLegacy(
|
||||
String type,
|
||||
boolean threading, boolean group_category,
|
||||
String sort1, String sort2, boolean ascending,
|
||||
boolean filter_seen, boolean filter_unflagged, boolean filter_unknown, boolean filter_snoozed, boolean filter_deleted, String filter_language,
|
||||
boolean found,
|
||||
boolean debug);
|
||||
|
||||
@Transaction
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
@Query("SELECT message.*" +
|
||||
", account.pop AS accountProtocol, account.name AS accountName, account.category AS accountCategory, COALESCE(identity.color, folder.color, account.color) AS accountColor" +
|
||||
", account.notify AS accountNotify, account.summary AS accountSummary, account.leave_deleted AS accountLeaveDeleted, account.auto_seen AS accountAutoSeen" +
|
||||
", folder.name AS folderName, folder.color AS folderColor, folder.display AS folderDisplay, folder.type AS folderType, f.inherited_type AS folderInheritedType, folder.unified AS folderUnified, folder.read_only AS folderReadOnly" +
|
||||
", IFNULL(identity.display, identity.name) AS identityName, identity.email AS identityEmail, identity.color AS identityColor, identity.synchronize AS identitySynchronize" +
|
||||
", '[' || substr(group_concat(message.`from`, ','), 0, 2048) || ']' AS senders" +
|
||||
", '[' || substr(group_concat(message.`to`, ','), 0, 2048) || ']' AS recipients" +
|
||||
", COUNT(message.id) AS count" +
|
||||
", SUM(1 - message.ui_seen) AS unseen" +
|
||||
", SUM(1 - message.ui_flagged) AS unflagged" +
|
||||
", SUM(folder.type = '" + EntityFolder.DRAFTS + "') AS drafts" +
|
||||
", COUNT(DISTINCT" +
|
||||
" CASE WHEN NOT message.hash IS NULL THEN message.hash" +
|
||||
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
|
||||
" ELSE message.id END) AS visible" +
|
||||
", COUNT(DISTINCT" +
|
||||
" CASE WHEN message.ui_seen THEN NULL" +
|
||||
" WHEN NOT message.hash IS NULL THEN message.hash" +
|
||||
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
|
||||
" ELSE message.id END) AS visible_unseen" +
|
||||
", SUM(message.attachments) AS totalAttachments" +
|
||||
", SUM(message.total) AS totalSize" +
|
||||
", message.priority AS ui_priority" +
|
||||
", message.importance AS ui_importance" +
|
||||
", MAX(CASE WHEN" +
|
||||
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
|
||||
" OR (NOT :found AND folder.id = :folder)" +
|
||||
" THEN message.received ELSE 0 END) AS dummy" +
|
||||
" FROM (SELECT * FROM message" +
|
||||
" WHERE message.thread IN" +
|
||||
" (SELECT DISTINCT mm.thread FROM message mm" +
|
||||
" WHERE mm.folder = :folder" +
|
||||
" AND (NOT mm.ui_hide OR :debug)" +
|
||||
" AND (NOT :found OR mm.ui_found))" +
|
||||
" ORDER BY received DESC) AS message" + // group_concat
|
||||
" JOIN account_view AS account ON account.id = message.account" +
|
||||
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
|
||||
" JOIN folder_view AS folder ON folder.id = message.folder" +
|
||||
" JOIN folder_view AS f ON f.id = :folder" +
|
||||
" WHERE (message.account = f.account OR message.account = identity.account OR " + is_outbox + ")" +
|
||||
" AND (:threading OR folder.id = :folder)" +
|
||||
" AND (NOT message.ui_hide OR :debug)" +
|
||||
" AND (NOT :found OR message.ui_found = :found)" +
|
||||
" GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
|
||||
" HAVING (SUM((:found AND message.ui_found)" +
|
||||
" OR (NOT :found AND message.folder = :folder)) > 0)" +
|
||||
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
|
||||
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
|
||||
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0 OR " + is_outbox + ")" +
|
||||
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0" +
|
||||
" OR " + is_outbox + " OR " + is_drafts + " OR " + is_sent + ")" +
|
||||
" AND (NOT :filter_snoozed OR message.ui_snoozed IS NULL OR " + is_outbox + " OR " + is_drafts + ")" +
|
||||
" AND (NOT :filter_deleted OR NOT message.ui_deleted)" +
|
||||
" AND (:filter_language IS NULL OR SUM(message.language = :filter_language) > 0 OR " + is_outbox + ")" +
|
||||
" ORDER BY CASE WHEN :found THEN 0 ELSE -IFNULL(message.importance, 1) END" +
|
||||
", CASE" +
|
||||
" WHEN 'unread' = :sort1 THEN SUM(1 - message.ui_seen) = 0" +
|
||||
" WHEN 'starred' = :sort1 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
|
||||
" WHEN 'priority' = :sort1 THEN -IFNULL(message.priority, 1)" +
|
||||
" WHEN 'sender' = :sort1 THEN LOWER(message.sender)" +
|
||||
" WHEN 'subject' = :sort1 THEN LOWER(message.subject)" +
|
||||
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
|
||||
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
|
||||
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
|
||||
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE" +
|
||||
" WHEN 'unread' = :sort2 THEN SUM(1 - message.ui_seen) = 0" +
|
||||
" WHEN 'starred' = :sort2 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
|
||||
" ELSE 0" +
|
||||
" END" +
|
||||
", CASE WHEN :ascending THEN message.received ELSE -message.received END")
|
||||
DataSource.Factory<Integer, TupleMessageEx> pagedFolderLegacy(
|
||||
long folder, boolean threading,
|
||||
String sort1, String sort2, boolean ascending,
|
||||
boolean filter_seen, boolean filter_unflagged, boolean filter_unknown, boolean filter_snoozed, boolean filter_deleted, String filter_language,
|
||||
boolean found,
|
||||
boolean debug);
|
||||
}
|
|
@ -43,6 +43,7 @@ public interface DaoOperation {
|
|||
" WHEN operation.name = '" + EntityOperation.DOWNLOAD + "' THEN 3" +
|
||||
" WHEN operation.name = '" + EntityOperation.EXISTS + "' THEN 3" +
|
||||
" WHEN operation.name = '" + EntityOperation.REPORT + "' THEN 3" +
|
||||
" WHEN operation.name = '" + EntityOperation.SUBJECT + "' THEN 3" +
|
||||
" WHEN operation.name = '" + EntityOperation.COPY + "' THEN 4" +
|
||||
" WHEN operation.name = '" + EntityOperation.MOVE + "' THEN 5" +
|
||||
" WHEN operation.name = '" + EntityOperation.PURGE + "' THEN 6" +
|
||||
|
|
|
@ -891,6 +891,7 @@ public class DebugHelper {
|
|||
identity.display + " " + identity.email +
|
||||
(identity.self ? "" : " !self") +
|
||||
" [" + (identity.provider == null ? "" : identity.provider) +
|
||||
":" + identity.user +
|
||||
":" + ServiceAuthenticator.getAuthTypeName(identity.auth_type) + "]" +
|
||||
(TextUtils.isEmpty(identity.sender_extra_regex) ? "" : " regex=" + identity.sender_extra_regex) +
|
||||
(!identity.sender_extra ? "" : " edit" +
|
||||
|
@ -1627,7 +1628,7 @@ public class DebugHelper {
|
|||
sb.append(scheme);
|
||||
}
|
||||
|
||||
if (tabs && BuildConfig.DEBUG)
|
||||
if (tabs && BuildConfig.DEBUG && false)
|
||||
try {
|
||||
boolean bindable = context.bindService(serviceIntent, new CustomTabsServiceConnection() {
|
||||
@Override
|
||||
|
|
|
@ -62,7 +62,6 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
@ -566,7 +565,7 @@ public class DnsHelper {
|
|||
request.connect();
|
||||
|
||||
int status = request.getResponseCode();
|
||||
if (status != HttpURLConnection.HTTP_OK)
|
||||
if (status != HttpsURLConnection.HTTP_OK)
|
||||
throw new IOException("Error " + status + ": " + request.getResponseMessage());
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
|
|
@ -332,6 +332,10 @@ public class EmailService implements AutoCloseable {
|
|||
properties.put("mail." + protocol + ".ignorebodystructuresize", Boolean.toString(enabled));
|
||||
}
|
||||
|
||||
void setMailFrom(String address) {
|
||||
properties.put("mail." + protocol + ".from", address);
|
||||
}
|
||||
|
||||
void setSendPartial(boolean enabled) {
|
||||
properties.put("mail." + protocol + ".sendpartial", Boolean.toString(enabled));
|
||||
}
|
||||
|
@ -516,7 +520,7 @@ public class EmailService implements AutoCloseable {
|
|||
|
||||
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) {
|
||||
try {
|
||||
EntityLog.log(context, EntityLog.Type.Debug,
|
||||
EntityLog.log(context, EntityLog.Type.Debug1,
|
||||
ex + "\n" + android.util.Log.getStackTraceString(ex));
|
||||
authenticator.refreshToken(true);
|
||||
connect(dnssec, host, port, auth, user, factory);
|
||||
|
|
|
@ -39,7 +39,6 @@ import org.json.JSONObject;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -156,21 +155,32 @@ public class EntityContact implements Serializable {
|
|||
if (type == TYPE_FROM) {
|
||||
if (message.reply == null || message.reply.length == 0) {
|
||||
if (message.from != null)
|
||||
addresses.addAll(Arrays.asList(message.from));
|
||||
addresses.addAll(filterAddresses(message.from));
|
||||
} else
|
||||
addresses.addAll(Arrays.asList(message.reply));
|
||||
addresses.addAll(filterAddresses(message.reply));
|
||||
} else if (type == TYPE_TO) {
|
||||
if (message.to != null)
|
||||
addresses.addAll(Arrays.asList(message.to));
|
||||
addresses.addAll(filterAddresses(message.to));
|
||||
if (message.cc != null)
|
||||
addresses.addAll(Arrays.asList(message.cc));
|
||||
addresses.addAll(filterAddresses(message.cc));
|
||||
if (message.bcc != null)
|
||||
addresses.addAll(Arrays.asList(message.bcc));
|
||||
addresses.addAll(filterAddresses(message.bcc));
|
||||
}
|
||||
|
||||
update(context, folder.account, message.identity, addresses.toArray(new Address[0]), type, message.received);
|
||||
}
|
||||
|
||||
private static List<Address> filterAddresses(Address[] addresses) {
|
||||
List<Address> result = new ArrayList<>();
|
||||
|
||||
if (addresses != null)
|
||||
for (Address address : addresses)
|
||||
if (!MessageHelper.isNoReply(address))
|
||||
result.add(address);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void update(Context context, long account, Long identity, Address[] addresses, int type, long time) {
|
||||
update(context, account, identity, addresses, null, type, time);
|
||||
}
|
||||
|
|
|
@ -415,6 +415,16 @@ public class EntityFolder extends EntityOrder implements Serializable {
|
|||
return outbox;
|
||||
}
|
||||
|
||||
static List<EntityFolder> getChildFolders(Context context, long id) {
|
||||
DB db = DB.getInstance(context);
|
||||
List<EntityFolder> children = db.folder().getChildFolders(id);
|
||||
if (children == null)
|
||||
children = new ArrayList<>();
|
||||
for (EntityFolder child : new ArrayList<>(children))
|
||||
children.addAll(getChildFolders(context, child.id));
|
||||
return children;
|
||||
}
|
||||
|
||||
static String getNotificationChannelId(long id) {
|
||||
return "notification.folder." + id;
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ public class EntityIdentity {
|
|||
public String replyto;
|
||||
public String cc;
|
||||
public String bcc;
|
||||
public String envelopeFrom;
|
||||
public String internal;
|
||||
public String uri; // linked contact
|
||||
@NonNull
|
||||
|
|
|
@ -49,7 +49,7 @@ public class EntityLog {
|
|||
private static Long last_cleanup = null;
|
||||
|
||||
private static final long LOG_CLEANUP_INTERVAL = 3600 * 1000L; // milliseconds
|
||||
private static final long LOG_KEEP_DURATION = 12 * 3600 * 1000L; // milliseconds
|
||||
private static final long LOG_KEEP_DURATION = (BuildConfig.DEBUG ? 24 : 12) * 3600 * 1000L; // milliseconds
|
||||
private static final int LOG_DELETE_BATCH_SIZE = 50;
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -65,7 +65,7 @@ public class EntityLog {
|
|||
@NonNull
|
||||
public String data;
|
||||
|
||||
public enum Type {General, Statistics, Scheduling, Network, Account, Protocol, Classification, Notification, Rules, Cloud, Debug}
|
||||
public enum Type {General, Statistics, Scheduling, Network, Account, Protocol, Classification, Notification, Rules, Cloud, Debug1, Debug2, Debug3}
|
||||
|
||||
public static void log(final Context context, String data) {
|
||||
log(context, Type.General, data);
|
||||
|
@ -109,7 +109,7 @@ public class EntityLog {
|
|||
|
||||
if (context == null)
|
||||
return;
|
||||
if (type == Type.Debug &&
|
||||
if ((type == Type.Debug1 || type == Type.Debug2 || type == Type.Debug3) &&
|
||||
!(BuildConfig.DEBUG || Log.isTestRelease()))
|
||||
return;
|
||||
|
||||
|
@ -233,8 +233,12 @@ public class EntityLog {
|
|||
return ContextCompat.getColor(context, R.color.solarizedCyan);
|
||||
case Cloud:
|
||||
return ContextCompat.getColor(context, R.color.solarizedRed);
|
||||
case Debug:
|
||||
return Helper.resolveColor(context, R.attr.colorWarning);
|
||||
case Debug1:
|
||||
return ContextCompat.getColor(context, R.color.solarizedRed);
|
||||
case Debug2:
|
||||
return ContextCompat.getColor(context, R.color.solarizedGreen);
|
||||
case Debug3:
|
||||
return ContextCompat.getColor(context, R.color.solarizedBlue);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,7 @@ public class EntityMessage implements Serializable {
|
|||
static final Long SWIPE_ACTION_JUNK = -8L;
|
||||
static final Long SWIPE_ACTION_REPLY = -9L;
|
||||
static final Long SWIPE_ACTION_IMPORTANCE = -10L;
|
||||
static final Long SWIPE_ACTION_SUMMARIZE = -11L;
|
||||
|
||||
private static final int MAX_SNOOZED = 300;
|
||||
|
||||
|
@ -262,6 +263,7 @@ public class EntityMessage implements Serializable {
|
|||
public String warning; // persistent
|
||||
public String error; // volatile
|
||||
public Long last_attempt; // send
|
||||
public Long last_touched;
|
||||
|
||||
static String generateMessageId() {
|
||||
return generateMessageId("localhost");
|
||||
|
@ -753,6 +755,8 @@ public class EntityMessage implements Serializable {
|
|||
return "junk";
|
||||
if (SWIPE_ACTION_REPLY.equals(type))
|
||||
return "reply";
|
||||
if (SWIPE_ACTION_SUMMARIZE.equals(type))
|
||||
return "summarize";
|
||||
return "???";
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,6 @@ package eu.faircode.email;
|
|||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
|
||||
import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
|
@ -40,20 +38,13 @@ import androidx.annotation.NonNull;
|
|||
import androidx.preference.PreferenceManager;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.ezylang.evalex.EvaluationException;
|
||||
import com.ezylang.evalex.Expression;
|
||||
import com.ezylang.evalex.config.ExpressionConfiguration;
|
||||
import com.ezylang.evalex.data.EvaluationValue;
|
||||
import com.ezylang.evalex.functions.AbstractFunction;
|
||||
import com.ezylang.evalex.functions.FunctionParameter;
|
||||
import com.ezylang.evalex.operators.AbstractOperator;
|
||||
import com.ezylang.evalex.operators.InfixOperator;
|
||||
import com.ezylang.evalex.parser.ASTNode;
|
||||
import com.ezylang.evalex.parser.ParseException;
|
||||
import com.ezylang.evalex.parser.Token;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -64,8 +55,6 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
|
@ -75,11 +64,9 @@ import java.util.Arrays;
|
|||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -132,6 +119,9 @@ public class EntityRule {
|
|||
public Integer applied = 0;
|
||||
public Long last_applied;
|
||||
|
||||
@Ignore
|
||||
public boolean async;
|
||||
|
||||
static final int TYPE_SEEN = 1;
|
||||
static final int TYPE_UNSEEN = 2;
|
||||
static final int TYPE_MOVE = 3;
|
||||
|
@ -152,6 +142,7 @@ public class EntityRule {
|
|||
static final int TYPE_NOTES = 18;
|
||||
static final int TYPE_URL = 19;
|
||||
static final int TYPE_SILENT = 20;
|
||||
static final int TYPE_SUMMARIZE = 21;
|
||||
|
||||
static final String ACTION_AUTOMATION = BuildConfig.APPLICATION_ID + ".AUTOMATION";
|
||||
static final String EXTRA_RULE = "rule";
|
||||
|
@ -169,10 +160,6 @@ public class EntityRule {
|
|||
private static final int MAX_NOTES_LENGTH = 512; // characters
|
||||
private static final int URL_TIMEOUT = 15 * 1000; // milliseconds
|
||||
|
||||
private static final List<String> EXPR_VARIABLES = Collections.unmodifiableList(Arrays.asList(
|
||||
"to", "from", "subject", "text", "hasAttachments"
|
||||
));
|
||||
|
||||
static boolean needsHeaders(EntityMessage message, List<EntityRule> rules) {
|
||||
return needsHeaders(rules);
|
||||
}
|
||||
|
@ -208,11 +195,11 @@ public class EntityRule {
|
|||
}
|
||||
|
||||
if (jcondition.has("expression")) {
|
||||
Expression expression = getExpression(rule, null, null, null, null);
|
||||
Expression expression = ExpressionHelper.getExpression(rule, null, null, null, null);
|
||||
if (expression != null) {
|
||||
if ("header".equals(what) && needsHeaders(expression))
|
||||
if ("header".equals(what) && ExpressionHelper.needsHeaders(expression))
|
||||
return true;
|
||||
if ("body".equals(what) && needsBody(expression))
|
||||
if ("body".equals(what) && ExpressionHelper.needsBody(expression))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +430,7 @@ public class EntityRule {
|
|||
|
||||
Document d = JsoupEx.parse(html);
|
||||
if (skip_quotes)
|
||||
d.select("blockquote").remove();
|
||||
HtmlHelper.removeQuotes(d);
|
||||
if (jsoup) {
|
||||
String selector = value.substring(JSOUP_PREFIX.length());
|
||||
if (d.select(selector).isEmpty() != not)
|
||||
|
@ -498,9 +485,9 @@ public class EntityRule {
|
|||
}
|
||||
|
||||
// Expression
|
||||
Expression expression = getExpression(this, message, headers, html, context);
|
||||
Expression expression = ExpressionHelper.getExpression(this, message, headers, html, context);
|
||||
if (expression != null) {
|
||||
if (needsHeaders(expression) && headers == null && message.headers == null)
|
||||
if (ExpressionHelper.needsHeaders(expression) && headers == null && message.headers == null)
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_rule_no_headers));
|
||||
|
||||
Log.i("EXPR evaluating='" + jcondition.getString("expression") + "'");
|
||||
|
@ -641,244 +628,6 @@ public class EntityRule {
|
|||
return matched;
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class HeaderFunction extends AbstractFunction {
|
||||
private List<Header> headers;
|
||||
|
||||
HeaderFunction(List<Header> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (parameterValues.length == 1) {
|
||||
String name = parameterValues[0].getStringValue();
|
||||
if (name != null && headers != null)
|
||||
for (Header header : headers)
|
||||
if (name.equalsIgnoreCase(header.getName()))
|
||||
result.add(header.getValue());
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR header(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
|
||||
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class MessageFunction extends AbstractFunction {
|
||||
private EntityMessage message;
|
||||
|
||||
MessageFunction(EntityMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
List<Object> result = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (parameterValues.length == 1) {
|
||||
String name = parameterValues[0].getStringValue();
|
||||
if (name != null && message != null) {
|
||||
Field field = message.getClass().getField(name);
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(message);
|
||||
if (value != null)
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR message(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
|
||||
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
public static class BlocklistFunction extends AbstractFunction {
|
||||
private Context context;
|
||||
private Address[] from;
|
||||
private List<Header> headers;
|
||||
|
||||
BlocklistFunction(Context context, Address[] from, List<Header> headers) {
|
||||
this.context = context;
|
||||
this.from = from;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
if (from != null)
|
||||
result = Boolean.TRUE.equals(DnsBlockList.isJunk(context, Arrays.asList(from)));
|
||||
|
||||
List<String> received = new ArrayList<>();
|
||||
if (headers != null)
|
||||
for (Header header : headers)
|
||||
if (header.getName().equalsIgnoreCase("Received"))
|
||||
received.add(header.getValue());
|
||||
result = result || Boolean.TRUE.equals(DnsBlockList.isJunk(context, received.toArray(new String[0])));
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR blocklist()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
|
||||
public static class ContainsOperator extends AbstractOperator {
|
||||
private boolean regex;
|
||||
|
||||
ContainsOperator(boolean regex) {
|
||||
this.regex = regex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token operatorToken, EvaluationValue... operands) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
if (operands.length == 2) {
|
||||
List<EvaluationValue> array;
|
||||
if (operands[1].getDataType() == EvaluationValue.DataType.ARRAY)
|
||||
array = operands[0].getArrayValue();
|
||||
else
|
||||
array = Arrays.asList(operands[0]);
|
||||
|
||||
String condition = operands[1].getStringValue();
|
||||
|
||||
if (array != null && !array.isEmpty() && !TextUtils.isEmpty(condition))
|
||||
for (EvaluationValue item : array) {
|
||||
String value = item.getStringValue();
|
||||
if (!TextUtils.isEmpty(value))
|
||||
if (regex
|
||||
? Pattern.compile(condition, Pattern.DOTALL).matcher(value).matches()
|
||||
: value.toLowerCase().contains(condition.toLowerCase())) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR " + operands[0] + (regex ? " MATCHES " : " CONTAINS ") + operands[1] +
|
||||
" regex=" + regex + " result=" + result);
|
||||
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
static Expression getExpression(EntityRule rule, EntityMessage message, List<Header> headers, String html, Context context) throws JSONException, ParseException, MessagingException {
|
||||
// https://ezylang.github.io/EvalEx/
|
||||
|
||||
JSONObject jcondition = new JSONObject(rule.condition);
|
||||
if (!jcondition.has("expression"))
|
||||
return null;
|
||||
String eval = jcondition.getString("expression");
|
||||
|
||||
List<String> to = new ArrayList<>();
|
||||
if (message != null && message.to != null)
|
||||
for (Address a : message.to)
|
||||
to.add(MessageHelper.formatAddresses(new Address[]{a}));
|
||||
|
||||
List<String> from = new ArrayList<>();
|
||||
if (message != null && message.from != null)
|
||||
for (Address a : message.from)
|
||||
from.add(MessageHelper.formatAddresses(new Address[]{a}));
|
||||
|
||||
if (html == null && message != null && message.content)
|
||||
try {
|
||||
html = Helper.readText(message.getFile(context));
|
||||
} catch (IOException ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
Document doc = (html == null ? null : JsoupEx.parse(html));
|
||||
|
||||
if (headers == null && message != null && message.headers != null) {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(message.headers.getBytes());
|
||||
headers = Collections.list(new InternetHeaders(bis, true).getAllHeaders());
|
||||
}
|
||||
|
||||
ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration();
|
||||
configuration.getFunctionDictionary().addFunction("Header",
|
||||
new HeaderFunction(headers));
|
||||
configuration.getFunctionDictionary().addFunction("Message",
|
||||
new MessageFunction(message));
|
||||
configuration.getFunctionDictionary().addFunction("Blocklist",
|
||||
new BlocklistFunction(context, message == null ? null : message.from, headers));
|
||||
configuration.getOperatorDictionary().addOperator("Contains",
|
||||
new ContainsOperator(false));
|
||||
configuration.getOperatorDictionary().addOperator("Matches",
|
||||
new ContainsOperator(true));
|
||||
|
||||
Expression expression = new Expression(eval, configuration)
|
||||
.with("to", to)
|
||||
.with("from", from)
|
||||
.with("subject", message == null ? null : message.subject)
|
||||
.with("text", doc == null ? null : doc.text());
|
||||
|
||||
if (message != null) {
|
||||
boolean hasAttachments = false;
|
||||
for (String variable : expression.getUsedVariables())
|
||||
if (!hasAttachments && "hasAttachments".equals(variable)) {
|
||||
hasAttachments = true;
|
||||
DB db = DB.getInstance(context);
|
||||
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
|
||||
expression.with("hasAttachments", attachments != null && !attachments.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
static boolean needsHeaders(Expression expression) {
|
||||
try {
|
||||
expression.validate();
|
||||
for (ASTNode node : expression.getAllASTNodes()) {
|
||||
Token token = node.getToken();
|
||||
Log.i("EXPR token=" + token.getType() + ":" + token.getValue());
|
||||
if (token.getType() == Token.TokenType.FUNCTION &&
|
||||
("header".equalsIgnoreCase(token.getValue()) ||
|
||||
"blocklist".equalsIgnoreCase(token.getValue()))) {
|
||||
Log.i("EXPR needs headers");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean needsBody(Expression expression) {
|
||||
try {
|
||||
for (String variable : expression.getUsedVariables())
|
||||
if ("text".equalsIgnoreCase(variable))
|
||||
return true;
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean execute(Context context, EntityMessage message, String html) throws JSONException, IOException {
|
||||
boolean executed = _execute(context, message, html);
|
||||
if (this.id != null && executed) {
|
||||
|
@ -935,6 +684,8 @@ public class EntityRule {
|
|||
return onActionUrl(context, message, jaction, html);
|
||||
case TYPE_SILENT:
|
||||
return onActionSilent(context, message, jaction);
|
||||
case TYPE_SUMMARIZE:
|
||||
return onActionSummarize(context, message, jaction);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown rule type=" + type + " name=" + name);
|
||||
}
|
||||
|
@ -942,17 +693,9 @@ public class EntityRule {
|
|||
|
||||
void validate(Context context) throws JSONException, IllegalArgumentException {
|
||||
try {
|
||||
Expression expression = getExpression(this, null, null, null, context);
|
||||
if (expression != null) {
|
||||
for (String variable : expression.getUsedVariables()) {
|
||||
Log.i("EXPR variable=" + variable);
|
||||
if (!EXPR_VARIABLES.contains(variable))
|
||||
throw new IllegalArgumentException("Unknown variable '" + variable + "'");
|
||||
}
|
||||
Log.i("EXPR validating");
|
||||
expression.validate();
|
||||
Log.i("EXPR validated");
|
||||
}
|
||||
Expression expression = ExpressionHelper.getExpression(this, null, null, null, context);
|
||||
if (expression != null)
|
||||
ExpressionHelper.check(expression);
|
||||
} catch (ParseException | MessagingException ex) {
|
||||
Log.w("EXPR", ex);
|
||||
String message = ex.getMessage();
|
||||
|
@ -1045,6 +788,8 @@ public class EntityRule {
|
|||
return;
|
||||
case TYPE_SILENT:
|
||||
return;
|
||||
case TYPE_SUMMARIZE:
|
||||
return;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown rule type=" + type);
|
||||
}
|
||||
|
@ -1444,7 +1189,7 @@ public class EntityRule {
|
|||
|
||||
File file = reply.getFile(context);
|
||||
Helper.writeText(file, body);
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, true);
|
||||
reply.preview = HtmlHelper.getPreview(text);
|
||||
reply.language = HtmlHelper.getLanguage(context, reply.subject, text);
|
||||
db.message().setMessageContent(reply.id,
|
||||
|
@ -1556,7 +1301,7 @@ public class EntityRule {
|
|||
.append(' ').append(message.subject).append(". ");
|
||||
|
||||
String body = Helper.readText(message.getFile(context));
|
||||
String text = HtmlHelper.getFullText(body);
|
||||
String text = HtmlHelper.getFullText(body, false);
|
||||
String preview = HtmlHelper.getPreview(text);
|
||||
|
||||
if (!TextUtils.isEmpty(preview))
|
||||
|
@ -1818,6 +1563,22 @@ public class EntityRule {
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean onActionSummarize(Context context, EntityMessage message, JSONObject jargs) throws JSONException, IOException {
|
||||
DB db = DB.getInstance(context);
|
||||
|
||||
if (!this.async && this.id != null) {
|
||||
EntityOperation.queue(context, message, EntityOperation.RULE, this.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
message.preview = AI.getSummaryText(context, message);
|
||||
|
||||
db.message().setMessageContent(message.id, message.content, message.language, message.plain_only, message.preview, message.warning);
|
||||
db.message().setMessageNotifying(message.id, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Calendar getRelativeCalendar(boolean all, int minutes, long reference) {
|
||||
int d = minutes / (24 * 60);
|
||||
int h = minutes / 60 % 24;
|
||||
|
|
|
@ -0,0 +1,481 @@
|
|||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of FairEmail.
|
||||
|
||||
FairEmail 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.
|
||||
|
||||
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2024 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.ezylang.evalex.Expression;
|
||||
import com.ezylang.evalex.config.ExpressionConfiguration;
|
||||
import com.ezylang.evalex.data.EvaluationValue;
|
||||
import com.ezylang.evalex.functions.AbstractFunction;
|
||||
import com.ezylang.evalex.functions.FunctionParameter;
|
||||
import com.ezylang.evalex.operators.AbstractOperator;
|
||||
import com.ezylang.evalex.operators.InfixOperator;
|
||||
import com.ezylang.evalex.parser.ASTNode;
|
||||
import com.ezylang.evalex.parser.ParseException;
|
||||
import com.ezylang.evalex.parser.Token;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.mail.Address;
|
||||
import javax.mail.Header;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.InternetHeaders;
|
||||
|
||||
public class ExpressionHelper {
|
||||
private static final List<String> EXPR_VARIABLES = Collections.unmodifiableList(Arrays.asList(
|
||||
"received", "to", "from", "subject", "text", "hasAttachments"
|
||||
));
|
||||
|
||||
static void check(Expression expression) throws ParseException {
|
||||
for (String variable : expression.getUsedVariables()) {
|
||||
Log.i("EXPR variable=" + variable);
|
||||
if (!EXPR_VARIABLES.contains(variable))
|
||||
throw new IllegalArgumentException("Unknown variable '" + variable + "'");
|
||||
}
|
||||
Log.i("EXPR validating");
|
||||
expression.validate();
|
||||
Log.i("EXPR validated");
|
||||
}
|
||||
|
||||
static Expression getExpression(EntityRule rule, EntityMessage message, List<Header> headers, String html, Context context) throws JSONException, ParseException, MessagingException {
|
||||
// https://ezylang.github.io/EvalEx/
|
||||
|
||||
JSONObject jcondition = new JSONObject(rule.condition);
|
||||
if (!jcondition.has("expression"))
|
||||
return null;
|
||||
String eval = jcondition.getString("expression");
|
||||
|
||||
List<String> to = new ArrayList<>();
|
||||
if (message != null && message.to != null)
|
||||
for (Address a : message.to)
|
||||
to.add(MessageHelper.formatAddresses(new Address[]{a}));
|
||||
|
||||
List<String> from = new ArrayList<>();
|
||||
if (message != null && message.from != null)
|
||||
for (Address a : message.from)
|
||||
from.add(MessageHelper.formatAddresses(new Address[]{a}));
|
||||
|
||||
if (html == null && message != null && message.content)
|
||||
try {
|
||||
html = Helper.readText(message.getFile(context));
|
||||
} catch (IOException ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
Document doc = (html == null ? null : JsoupEx.parse(html));
|
||||
|
||||
if (headers == null && message != null && message.headers != null) {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(message.headers.getBytes());
|
||||
headers = Collections.list(new InternetHeaders(bis, true).getAllHeaders());
|
||||
}
|
||||
|
||||
HeaderFunction fHeader = new HeaderFunction(headers);
|
||||
MessageFunction fMessage = new MessageFunction(message);
|
||||
BlocklistFunction fBlocklist = new BlocklistFunction(context, message, headers);
|
||||
MxFunction fMx = new MxFunction(context, message);
|
||||
AttachmentsFunction fAttachments = new AttachmentsFunction(context, message);
|
||||
JsoupFunction fJsoup = new JsoupFunction(context, message);
|
||||
SizeFunction fSize = new SizeFunction();
|
||||
KnownFunction fKnown = new KnownFunction(context, message);
|
||||
|
||||
ContainsOperator oContains = new ContainsOperator(false);
|
||||
ContainsOperator oMatches = new ContainsOperator(true);
|
||||
|
||||
ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration();
|
||||
|
||||
configuration.getFunctionDictionary().addFunction("Header", fHeader);
|
||||
configuration.getFunctionDictionary().addFunction("Message", fMessage);
|
||||
configuration.getFunctionDictionary().addFunction("Blocklist", fBlocklist);
|
||||
configuration.getFunctionDictionary().addFunction("onBlocklist", fBlocklist);
|
||||
configuration.getFunctionDictionary().addFunction("hasMx", fMx);
|
||||
configuration.getFunctionDictionary().addFunction("attachments", fAttachments);
|
||||
configuration.getFunctionDictionary().addFunction("Jsoup", fJsoup);
|
||||
configuration.getFunctionDictionary().addFunction("Size", fSize);
|
||||
configuration.getFunctionDictionary().addFunction("knownContact", fKnown);
|
||||
|
||||
configuration.getOperatorDictionary().addOperator("Contains", oContains);
|
||||
configuration.getOperatorDictionary().addOperator("Matches", oMatches);
|
||||
|
||||
Expression expression = new Expression(eval, configuration)
|
||||
.with("received", message == null ? null : message.received)
|
||||
.with("to", to)
|
||||
.with("from", from)
|
||||
.with("subject", message == null ? null : message.subject)
|
||||
.with("text", doc == null ? null : doc.text());
|
||||
|
||||
if (message != null) {
|
||||
boolean hasAttachments = false;
|
||||
for (String variable : expression.getUsedVariables())
|
||||
if (!hasAttachments && "hasAttachments".equals(variable)) {
|
||||
hasAttachments = true;
|
||||
DB db = DB.getInstance(context);
|
||||
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
|
||||
expression.with("hasAttachments", attachments != null && !attachments.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
static boolean needsHeaders(Expression expression) {
|
||||
try {
|
||||
expression.validate();
|
||||
for (ASTNode node : expression.getAllASTNodes()) {
|
||||
Token token = node.getToken();
|
||||
Log.i("EXPR token=" + token.getType() + ":" + token.getValue());
|
||||
if (token.getType() == Token.TokenType.FUNCTION &&
|
||||
("header".equalsIgnoreCase(token.getValue()) ||
|
||||
"blocklist".equalsIgnoreCase(token.getValue()))) {
|
||||
Log.i("EXPR needs headers");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean needsBody(Expression expression) {
|
||||
try {
|
||||
for (String variable : expression.getUsedVariables())
|
||||
if ("text".equalsIgnoreCase(variable))
|
||||
return true;
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class HeaderFunction extends AbstractFunction {
|
||||
private final List<Header> headers;
|
||||
|
||||
HeaderFunction(List<Header> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (parameterValues.length == 1) {
|
||||
String name = parameterValues[0].getStringValue();
|
||||
if (name != null && headers != null)
|
||||
for (Header header : headers)
|
||||
if (name.equalsIgnoreCase(header.getName()))
|
||||
result.add(header.getValue());
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR header(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
|
||||
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class MessageFunction extends AbstractFunction {
|
||||
private final EntityMessage message;
|
||||
|
||||
MessageFunction(EntityMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
List<Object> result = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (parameterValues.length == 1) {
|
||||
String name = parameterValues[0].getStringValue();
|
||||
if (name != null && message != null) {
|
||||
Field field = message.getClass().getField(name);
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(message);
|
||||
if (value != null)
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR message(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
|
||||
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
public static class BlocklistFunction extends AbstractFunction {
|
||||
private final Context context;
|
||||
private final List<Header> headers;
|
||||
private final EntityMessage message;
|
||||
|
||||
BlocklistFunction(Context context, EntityMessage message, List<Header> headers) {
|
||||
this.context = context;
|
||||
this.message = message;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
if (message != null && message.from != null)
|
||||
result = Boolean.TRUE.equals(DnsBlockList.isJunk(context, Arrays.asList(message.from)));
|
||||
|
||||
List<String> received = new ArrayList<>();
|
||||
if (headers != null)
|
||||
for (Header header : headers)
|
||||
if (header.getName().equalsIgnoreCase("Received"))
|
||||
received.add(header.getValue());
|
||||
result = result || Boolean.TRUE.equals(DnsBlockList.isJunk(context, received.toArray(new String[0])));
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR blocklist()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MxFunction extends AbstractFunction {
|
||||
private final Context context;
|
||||
private final EntityMessage message;
|
||||
|
||||
MxFunction(Context context, EntityMessage message) {
|
||||
this.context = context;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
Address[] addresses =
|
||||
(message.reply == null || message.reply.length == 0
|
||||
? message.from : message.reply);
|
||||
DnsHelper.checkMx(context, addresses);
|
||||
result = true;
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR mx()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AttachmentsFunction extends AbstractFunction {
|
||||
private final Context context;
|
||||
private final EntityMessage message;
|
||||
|
||||
AttachmentsFunction(Context context, EntityMessage message) {
|
||||
this.context = context;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
int result = 0;
|
||||
|
||||
if (message != null) {
|
||||
DB db = DB.getInstance(context);
|
||||
result = db.attachment().countAttachments(message.id);
|
||||
}
|
||||
|
||||
Log.i("EXPR attachments()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class JsoupFunction extends AbstractFunction {
|
||||
private final Context context;
|
||||
private final EntityMessage message;
|
||||
|
||||
JsoupFunction(Context context, EntityMessage message) {
|
||||
this.context = context;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
if (message != null && message.content && parameterValues.length == 1)
|
||||
try {
|
||||
String query = parameterValues[0].getStringValue();
|
||||
File file = message.getFile(context);
|
||||
Document d = JsoupEx.parse(file);
|
||||
for (Element element : d.select(query))
|
||||
result.add(element.text());
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR jsoup(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
|
||||
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionParameter(name = "value")
|
||||
public static class SizeFunction extends AbstractFunction {
|
||||
SizeFunction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
int result = 0;
|
||||
|
||||
if (parameterValues.length == 1 &&
|
||||
parameterValues[0].getDataType() == EvaluationValue.DataType.ARRAY)
|
||||
result = parameterValues[0].getArrayValue().size();
|
||||
|
||||
Log.i("EXPR size()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static class KnownFunction extends AbstractFunction {
|
||||
private final Context context;
|
||||
private final EntityMessage message;
|
||||
|
||||
KnownFunction(Context context, EntityMessage message) {
|
||||
this.context = context;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
|
||||
boolean result = false;
|
||||
|
||||
if (message != null)
|
||||
if (message.avatar != null)
|
||||
result = true;
|
||||
else {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean suggest_sent = prefs.getBoolean("suggest_sent", true);
|
||||
if (suggest_sent) {
|
||||
DB db = DB.getInstance(context);
|
||||
|
||||
List<Address> senders = new ArrayList<>();
|
||||
if (message.from != null)
|
||||
senders.addAll(Arrays.asList(message.from));
|
||||
if (message.reply != null)
|
||||
senders.addAll(Arrays.asList(message.reply));
|
||||
for (Address sender : senders) {
|
||||
InternetAddress ia = (InternetAddress) sender;
|
||||
String email = ia.getAddress();
|
||||
|
||||
EntityContact contact =
|
||||
db.contact().getContact(message.account, EntityContact.TYPE_TO, email);
|
||||
if (contact != null) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("EXPR known()=" + result);
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
|
||||
public static class ContainsOperator extends AbstractOperator {
|
||||
private final boolean regex;
|
||||
|
||||
ContainsOperator(boolean regex) {
|
||||
this.regex = regex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationValue evaluate(
|
||||
Expression expression, Token operatorToken, EvaluationValue... operands) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
if (operands.length == 2) {
|
||||
List<EvaluationValue> array;
|
||||
if (operands[1].getDataType() == EvaluationValue.DataType.ARRAY)
|
||||
array = operands[0].getArrayValue();
|
||||
else
|
||||
array = Arrays.asList(operands[0]);
|
||||
|
||||
String condition = operands[1].getStringValue();
|
||||
|
||||
if (array != null && !array.isEmpty() && !TextUtils.isEmpty(condition))
|
||||
for (EvaluationValue item : array) {
|
||||
String value = item.getStringValue();
|
||||
if (!TextUtils.isEmpty(value))
|
||||
if (regex
|
||||
? Pattern.compile(condition, Pattern.DOTALL).matcher(value).matches()
|
||||
: value.toLowerCase().contains(condition.toLowerCase())) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("EXPR", ex);
|
||||
}
|
||||
|
||||
Log.i("EXPR " + operands[0] + (regex ? " MATCHES " : " CONTAINS ") + operands[1] +
|
||||
" regex=" + regex + " result=" + result);
|
||||
|
||||
return expression.convertValue(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,10 +62,14 @@ public class FairEmailLoggingProvider extends TinylogLoggingProvider {
|
|||
}
|
||||
|
||||
static void setup(Context context) {
|
||||
System.setProperty("tinylog.directory",
|
||||
Helper.ensureExists(context, "logs").getAbsolutePath());
|
||||
try {
|
||||
System.setProperty("tinylog.directory",
|
||||
Helper.ensureExists(context, "logs").getAbsolutePath());
|
||||
|
||||
setLevel(context);
|
||||
setLevel(context);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static void setLevel(Context context) {
|
||||
|
|
|
@ -108,8 +108,6 @@ public class FragmentAbout extends FragmentBase {
|
|||
llContributors.addView(tv);
|
||||
}
|
||||
|
||||
FragmentDialogTheme.setBackground(context, view, false);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
|
@ -626,7 +626,6 @@ public class FragmentAccount extends FragmentBase {
|
|||
|
||||
// Initialize
|
||||
Helper.setViewsEnabled(view, false);
|
||||
FragmentDialogTheme.setBackground(getContext(), view, false);
|
||||
|
||||
tvGmailHint.setVisibility(View.GONE);
|
||||
|
||||
|
@ -2139,6 +2138,13 @@ public class FragmentAccount extends FragmentBase {
|
|||
importance.name = context.getString(R.string.title_set_importance);
|
||||
folders.add(importance);
|
||||
|
||||
if (AI.isAvailable(context)) {
|
||||
EntityFolder summarize = new EntityFolder();
|
||||
summarize.id = EntityMessage.SWIPE_ACTION_SUMMARIZE;
|
||||
summarize.name = context.getString(R.string.title_summarize);
|
||||
folders.add(summarize);
|
||||
}
|
||||
|
||||
EntityFolder move = new EntityFolder();
|
||||
move.id = EntityMessage.SWIPE_ACTION_MOVE;
|
||||
move.name = context.getString(R.string.title_move);
|
||||
|
|
|
@ -33,6 +33,7 @@ import android.graphics.Color;
|
|||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -271,7 +272,7 @@ public class FragmentAccounts extends FragmentBase {
|
|||
getContext(),
|
||||
getViewLifecycleOwner(),
|
||||
getParentFragmentManager(),
|
||||
fabCompose, -1L);
|
||||
fabCompose, -1L, -1L);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -290,8 +291,6 @@ public class FragmentAccounts extends FragmentBase {
|
|||
animator = Helper.getFabAnimator(fab, getViewLifecycleOwner());
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(getContext(), view, false);
|
||||
|
||||
if (settings) {
|
||||
fab.show();
|
||||
fabCompose.hide();
|
||||
|
@ -368,6 +367,7 @@ public class FragmentAccounts extends FragmentBase {
|
|||
menu.findItem(R.id.menu_show_folders).setVisible(!settings);
|
||||
menu.findItem(R.id.menu_theme).setVisible(!settings);
|
||||
menu.findItem(R.id.menu_force_sync).setVisible(!settings);
|
||||
menu.findItem(R.id.menu_pwned).setVisible(settings && !TextUtils.isEmpty(BuildConfig.PWNED_ENDPOINT));
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
@ -399,6 +399,9 @@ public class FragmentAccounts extends FragmentBase {
|
|||
} else if (itemId == R.id.menu_force_sync) {
|
||||
onMenuForceSync();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_pwned) {
|
||||
onMenuPwned();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
@ -475,6 +478,10 @@ public class FragmentAccounts extends FragmentBase {
|
|||
ToastEx.makeText(getContext(), R.string.title_executing, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void onMenuPwned() {
|
||||
new FragmentDialogPwned().show(getParentFragmentManager(), "pawned");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
|
|
@ -198,8 +198,6 @@ public class FragmentAnswer extends FragmentBase {
|
|||
});
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(context, view, true);
|
||||
|
||||
etLabel.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||
cbExternal.setVisibility(View.GONE);
|
||||
cbSnippet.setVisibility(View.GONE);
|
||||
|
|
|
@ -197,7 +197,6 @@ public class FragmentAnswers extends FragmentBase {
|
|||
});
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(getContext(), view, false);
|
||||
grpReady.setVisibility(View.GONE);
|
||||
pbWait.setVisibility(View.VISIBLE);
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import androidx.annotation.RequiresApi;
|
|||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
@ -101,20 +102,6 @@ public class FragmentBase extends Fragment {
|
|||
return null;
|
||||
}
|
||||
|
||||
protected boolean isActionBarShown() {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof ActivityBase)
|
||||
return ((ActivityBase) activity).isActionBarShown();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void showActionBar(boolean show) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof ActivityBase)
|
||||
((ActivityBase) activity).showActionBar(show);
|
||||
}
|
||||
|
||||
protected void setCount(String count) {
|
||||
this.count = count;
|
||||
updateSubtitle();
|
||||
|
@ -465,15 +452,16 @@ public class FragmentBase extends Fragment {
|
|||
AppCompatActivity activity = (AppCompatActivity) getActivity();
|
||||
if (activity != null && !isPane()) {
|
||||
ActionBar actionbar = activity.getSupportActionBar();
|
||||
if (actionbar != null)
|
||||
if ((actionbar.getDisplayOptions() & DISPLAY_SHOW_CUSTOM) == 0) {
|
||||
if (actionbar != null) {
|
||||
Toolbar toolbar = activity.findViewById(R.id.toolbar);
|
||||
if ((actionbar.getDisplayOptions() & DISPLAY_SHOW_CUSTOM) == 0 && toolbar == null) {
|
||||
actionbar.setTitle(title == null ? getString(R.string.app_name) : title);
|
||||
actionbar.setSubtitle(subtitle);
|
||||
} else {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
boolean list_count = prefs.getBoolean("list_count", false);
|
||||
|
||||
View custom = actionbar.getCustomView();
|
||||
View custom = (toolbar == null ? actionbar.getCustomView() : toolbar);
|
||||
TextView tvCount = custom.findViewById(R.id.count);
|
||||
TextView tvTitle = custom.findViewById(R.id.title);
|
||||
TextView tvSubtitle = custom.findViewById(R.id.subtitle);
|
||||
|
@ -487,6 +475,7 @@ public class FragmentBase extends Fragment {
|
|||
if (tvSubtitle != null)
|
||||
tvSubtitle.setText(subtitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1084,7 +1084,6 @@ public class FragmentCompose extends FragmentBase {
|
|||
|
||||
// Initialize
|
||||
setHasOptionsMenu(true);
|
||||
FragmentDialogTheme.setBackground(getContext(), view, true);
|
||||
|
||||
if (keyboard_no_fullscreen) {
|
||||
// https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_FULLSCREEN
|
||||
|
@ -1873,22 +1872,22 @@ public class FragmentCompose extends FragmentBase {
|
|||
final Context context = getContext();
|
||||
PopupMenuLifecycle.insertIcons(context, menu, false);
|
||||
|
||||
LayoutInflater infl = LayoutInflater.from(context);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
Context actionBarContext = (actionBar == null ? context : actionBar.getThemedContext());
|
||||
LayoutInflater infl = LayoutInflater.from(actionBarContext);
|
||||
|
||||
ImageButton ibOpenAi = (ImageButton) infl.inflate(R.layout.action_button, null);
|
||||
ibOpenAi.setId(View.generateViewId());
|
||||
ibOpenAi.setImageResource(R.drawable.twotone_smart_toy_24);
|
||||
ibOpenAi.setContentDescription("AI");
|
||||
ibOpenAi.setOnClickListener(new View.OnClickListener() {
|
||||
ImageButton ibAI = (ImageButton) infl.inflate(R.layout.action_button, null);
|
||||
ibAI.setId(View.generateViewId());
|
||||
ibAI.setImageResource(R.drawable.twotone_smart_toy_24);
|
||||
ibAI.setContentDescription("AI");
|
||||
ibAI.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (OpenAI.isAvailable(view.getContext()))
|
||||
onOpenAi();
|
||||
else if (Gemini.isAvailable(view.getContext()))
|
||||
onGemini();
|
||||
if (AI.isAvailable(context))
|
||||
onAI();
|
||||
}
|
||||
});
|
||||
menu.findItem(R.id.menu_openai).setActionView(ibOpenAi);
|
||||
menu.findItem(R.id.menu_ai).setActionView(ibAI);
|
||||
|
||||
View v = infl.inflate(R.layout.action_button_text, null);
|
||||
v.setId(View.generateViewId());
|
||||
|
@ -1953,9 +1952,9 @@ public class FragmentCompose extends FragmentBase {
|
|||
menu.findItem(R.id.menu_encrypt).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_translate).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_translate).setVisible(DeepL.isAvailable(context));
|
||||
menu.findItem(R.id.menu_openai).setEnabled(state == State.LOADED && !chatting);
|
||||
((ImageButton) menu.findItem(R.id.menu_openai).getActionView()).setEnabled(!chatting);
|
||||
menu.findItem(R.id.menu_openai).setVisible(OpenAI.isAvailable(context) || Gemini.isAvailable(context));
|
||||
menu.findItem(R.id.menu_ai).setEnabled(state == State.LOADED && !chatting);
|
||||
((ImageButton) menu.findItem(R.id.menu_ai).getActionView()).setEnabled(!chatting);
|
||||
menu.findItem(R.id.menu_ai).setVisible(AI.isAvailable(context));
|
||||
menu.findItem(R.id.menu_zoom).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_style).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_media).setEnabled(state == State.LOADED);
|
||||
|
@ -2640,19 +2639,18 @@ public class FragmentCompose extends FragmentBase {
|
|||
}.serial().execute(this, args, "compose:print");
|
||||
}
|
||||
|
||||
private void onOpenAi() {
|
||||
private void onAI() {
|
||||
int start = etBody.getSelectionStart();
|
||||
int end = etBody.getSelectionEnd();
|
||||
boolean selection = (start >= 0 && end > start);
|
||||
Editable edit = etBody.getText();
|
||||
String body = (selection ? edit.subSequence(start, end) : edit).toString().trim();
|
||||
CharSequence body = (selection ? edit.subSequence(start, end) : edit);
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", working);
|
||||
args.putString("body", body);
|
||||
args.putBoolean("selection", selection);
|
||||
args.putCharSequence("body", body);
|
||||
|
||||
new SimpleTask<OpenAI.Message[]>() {
|
||||
new SimpleTask<String>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
chatting = true;
|
||||
|
@ -2666,61 +2664,15 @@ public class FragmentCompose extends FragmentBase {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected OpenAI.Message[] onExecute(Context context, Bundle args) throws Throwable {
|
||||
protected String onExecute(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
String body = args.getString("body");
|
||||
boolean selection = args.getBoolean("selection");
|
||||
CharSequence body = args.getCharSequence("body");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
EntityMessage draft = db.message().getMessage(id);
|
||||
if (draft == null)
|
||||
return null;
|
||||
|
||||
List<EntityMessage> inreplyto;
|
||||
if (selection || TextUtils.isEmpty(draft.inreplyto))
|
||||
inreplyto = new ArrayList<>();
|
||||
else
|
||||
inreplyto = db.message().getMessagesByMsgId(draft.account, draft.inreplyto);
|
||||
|
||||
List<OpenAI.Message> result = new ArrayList<>();
|
||||
|
||||
if (inreplyto.size() > 0 && inreplyto.get(0).content) {
|
||||
String role = (MessageHelper.equalEmail(draft.from, inreplyto.get(0).from) ? "assistant" : "user");
|
||||
Document parsed = JsoupEx.parse(inreplyto.get(0).getFile(context));
|
||||
Document document = HtmlHelper.sanitizeView(context, parsed, false);
|
||||
Spanned spanned = HtmlHelper.fromDocument(context, document, null, null);
|
||||
result.add(new OpenAI.Message(role, OpenAI.truncateParagraphs(spanned.toString())));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(body))
|
||||
result.add(new OpenAI.Message("assistant", OpenAI.truncateParagraphs(body)));
|
||||
|
||||
if (result.size() == 0)
|
||||
return null;
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String model = prefs.getString("openai_model", "gpt-3.5-turbo");
|
||||
float temperature = prefs.getFloat("openai_temperature", 0.5f);
|
||||
boolean moderation = prefs.getBoolean("openai_moderation", false);
|
||||
|
||||
if (moderation)
|
||||
for (OpenAI.Message message : result)
|
||||
OpenAI.checkModeration(context, message.getContent());
|
||||
|
||||
OpenAI.Message[] completions =
|
||||
OpenAI.completeChat(context, model, result.toArray(new OpenAI.Message[0]), temperature, 1);
|
||||
|
||||
return completions;
|
||||
return AI.completeChat(context, id, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExecuted(Bundle args, OpenAI.Message[] messages) {
|
||||
if (messages == null || messages.length == 0)
|
||||
return;
|
||||
|
||||
String text = messages[0].getContent()
|
||||
.replaceAll("^\\n+", "").replaceAll("\\n+$", "");
|
||||
|
||||
protected void onExecuted(Bundle args, String completion) {
|
||||
Editable edit = etBody.getText();
|
||||
int start = etBody.getSelectionStart();
|
||||
int end = etBody.getSelectionEnd();
|
||||
|
@ -2730,17 +2682,17 @@ public class FragmentCompose extends FragmentBase {
|
|||
edit.delete(start, end);
|
||||
index = start;
|
||||
} else
|
||||
index = end;
|
||||
index = etBody.length();
|
||||
|
||||
if (index < 0)
|
||||
index = 0;
|
||||
if (index > 0 && edit.charAt(index - 1) != '\n')
|
||||
edit.insert(index++, "\n");
|
||||
|
||||
edit.insert(index, text + "\n");
|
||||
etBody.setSelection(index + text.length() + 1);
|
||||
edit.insert(index, completion + "\n");
|
||||
etBody.setSelection(index + completion.length() + 1);
|
||||
|
||||
StyleHelper.markAsInserted(edit, index, index + text.length() + 1);
|
||||
StyleHelper.markAsInserted(edit, index, index + completion.length() + 1);
|
||||
|
||||
if (args.containsKey("used") && args.containsKey("granted")) {
|
||||
double used = args.getDouble("used");
|
||||
|
@ -2753,81 +2705,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex, !(ex instanceof IOException));
|
||||
}
|
||||
}.serial().execute(this, args, "openai");
|
||||
}
|
||||
|
||||
private void onGemini() {
|
||||
int start = etBody.getSelectionStart();
|
||||
int end = etBody.getSelectionEnd();
|
||||
boolean selection = (start >= 0 && end > start);
|
||||
Editable edit = etBody.getText();
|
||||
String body = (selection ? edit.subSequence(start, end) : edit).toString().trim();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", working);
|
||||
args.putString("body", body);
|
||||
args.putBoolean("selection", selection);
|
||||
|
||||
new SimpleTask<String[]>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
chatting = true;
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bundle args) {
|
||||
chatting = false;
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] onExecute(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
String body = args.getString("body");
|
||||
boolean selection = args.getBoolean("selection");
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String model = prefs.getString("gemini_model", "gemini-pro");
|
||||
|
||||
return Gemini.generate(context, model, new String[]{Gemini.truncateParagraphs(body)});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExecuted(Bundle args, String[] result) {
|
||||
if (result == null || result.length == 0)
|
||||
return;
|
||||
|
||||
String text = result[0]
|
||||
.replaceAll("^\\n+", "").replaceAll("\\n+$", "");
|
||||
|
||||
Editable edit = etBody.getText();
|
||||
int start = etBody.getSelectionStart();
|
||||
int end = etBody.getSelectionEnd();
|
||||
|
||||
int index;
|
||||
if (etBody.hasSelection()) {
|
||||
edit.delete(start, end);
|
||||
index = start;
|
||||
} else
|
||||
index = end;
|
||||
|
||||
if (index < 0)
|
||||
index = 0;
|
||||
if (index > 0 && edit.charAt(index - 1) != '\n')
|
||||
edit.insert(index++, "\n");
|
||||
|
||||
edit.insert(index, text + "\n");
|
||||
etBody.setSelection(index + text.length() + 1);
|
||||
|
||||
StyleHelper.markAsInserted(edit, index, index + text.length() + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex, !(ex instanceof IOException));
|
||||
}
|
||||
}.serial().execute(this, args, "gemini");
|
||||
}.serial().execute(this, args, "AI");
|
||||
}
|
||||
|
||||
private void onTranslate(View anchor) {
|
||||
|
@ -6004,8 +5882,9 @@ public class FragmentCompose extends FragmentBase {
|
|||
!("list".equals(action) && TextUtils.isEmpty(selected_text)) &&
|
||||
!"dsn".equals(action)) {
|
||||
// Reply/forward
|
||||
Element reply = document.createElement("div");
|
||||
reply.attr("fairemail", "reference");
|
||||
Element reply = document.createElement("div")
|
||||
.addClass("fairemail_quote")
|
||||
.attr("fairemail", "reference");
|
||||
|
||||
// Build reply header
|
||||
boolean separate_reply = prefs.getBoolean("separate_reply", false);
|
||||
|
@ -6136,7 +6015,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
Helper.writeText(data.draft.getFile(context), html);
|
||||
Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
|
||||
|
||||
String text = HtmlHelper.getFullText(html);
|
||||
String text = HtmlHelper.getFullText(html, true);
|
||||
data.draft.preview = HtmlHelper.getPreview(text);
|
||||
data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
|
||||
db.message().setMessageContent(data.draft.id,
|
||||
|
@ -6323,7 +6202,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
Helper.writeText(file, html);
|
||||
Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
|
||||
|
||||
String text = HtmlHelper.getFullText(html);
|
||||
String text = HtmlHelper.getFullText(html, true);
|
||||
data.draft.preview = HtmlHelper.getPreview(text);
|
||||
data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
|
||||
db.message().setMessageContent(data.draft.id,
|
||||
|
@ -7190,7 +7069,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
if (f.length() > MAX_REASONABLE_SIZE)
|
||||
args.putBoolean("large", true);
|
||||
|
||||
String full = HtmlHelper.getFullText(body);
|
||||
String full = HtmlHelper.getFullText(body, true);
|
||||
draft.preview = HtmlHelper.getPreview(full);
|
||||
draft.language = HtmlHelper.getLanguage(context, draft.subject, full);
|
||||
db.message().setMessageContent(draft.id,
|
||||
|
|
|
@ -57,6 +57,7 @@ public class FragmentDialogButtons extends FragmentDialogBase {
|
|||
final CheckBox cbSearch = dview.findViewById(R.id.cbSearch);
|
||||
final CheckBox cbSearchText = dview.findViewById(R.id.cbSearchText);
|
||||
final CheckBox cbTranslate = dview.findViewById(R.id.cbTranslate);
|
||||
final CheckBox cbSummarize = dview.findViewById(R.id.cbSummarize);
|
||||
final CheckBox cbFullScreen = dview.findViewById(R.id.cbFullScreen);
|
||||
final CheckBox cbForceLight = dview.findViewById(R.id.cbForceLight);
|
||||
final CheckBox cbEvent = dview.findViewById(R.id.cbEvent);
|
||||
|
@ -70,6 +71,7 @@ public class FragmentDialogButtons extends FragmentDialogBase {
|
|||
final CheckBox cbAnswer = dview.findViewById(R.id.cbAnswer);
|
||||
|
||||
cbTranslate.setVisibility(DeepL.isAvailable(context) ? View.VISIBLE : View.GONE);
|
||||
cbSummarize.setVisibility(AI.isAvailable(context) ? View.VISIBLE : View.GONE);
|
||||
cbPin.setVisibility(Shortcuts.can(context) ? View.VISIBLE : View.GONE);
|
||||
|
||||
cbSeen.setChecked(prefs.getBoolean("button_seen", false));
|
||||
|
@ -87,6 +89,7 @@ public class FragmentDialogButtons extends FragmentDialogBase {
|
|||
cbSearch.setChecked(prefs.getBoolean("button_search", false));
|
||||
cbSearchText.setChecked(prefs.getBoolean("button_search_text", false));
|
||||
cbTranslate.setChecked(prefs.getBoolean("button_translate", true));
|
||||
cbSummarize.setChecked(prefs.getBoolean("button_summarize", false));
|
||||
cbFullScreen.setChecked(prefs.getBoolean("button_full_screen", false));
|
||||
cbForceLight.setChecked(prefs.getBoolean("button_force_light", true));
|
||||
cbEvent.setChecked(prefs.getBoolean("button_event", false));
|
||||
|
@ -126,6 +129,7 @@ public class FragmentDialogButtons extends FragmentDialogBase {
|
|||
editor.putBoolean("button_search", cbSearch.isChecked());
|
||||
editor.putBoolean("button_search_text", cbSearchText.isChecked());
|
||||
editor.putBoolean("button_translate", cbTranslate.isChecked());
|
||||
editor.putBoolean("button_summarize", cbSummarize.isChecked());
|
||||
editor.putBoolean("button_full_screen", cbFullScreen.isChecked());
|
||||
editor.putBoolean("button_force_light", cbForceLight.isChecked());
|
||||
editor.putBoolean("button_event", cbEvent.isChecked());
|
||||
|
|
|
@ -35,6 +35,7 @@ public class FragmentDialogContactDelete extends FragmentDialogBase {
|
|||
return new AlertDialog.Builder(getContext())
|
||||
.setIcon(R.drawable.twotone_warning_24)
|
||||
.setTitle(getString(R.string.title_delete_contacts))
|
||||
.setMessage(getString(R.string.title_delete_contacts_remark))
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
|
|
@ -49,6 +49,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public class FragmentDialogIdentity extends FragmentDialogBase {
|
||||
private static final int MIN_IDENTITY_MESSAGES = 20;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -144,17 +146,26 @@ public class FragmentDialogIdentity extends FragmentDialogBase {
|
|||
AdapterIdentitySelect iadapter = new AdapterIdentitySelect(context, identities);
|
||||
spIdentity.setAdapter(iadapter);
|
||||
|
||||
long aid = args.getLong("account");
|
||||
long iid = args.getLong("identity", -1L);
|
||||
|
||||
Integer selected = null;
|
||||
long account = getArguments().getLong("account");
|
||||
for (int pos = 0; pos < identities.size(); pos++) {
|
||||
EntityIdentity identity = identities.get(pos);
|
||||
if (identity.account.equals(account)) {
|
||||
if (identity.primary) {
|
||||
if (iid < 0) {
|
||||
if (identity.account.equals(aid)) {
|
||||
if (identity.primary) {
|
||||
selected = pos;
|
||||
break;
|
||||
}
|
||||
if (selected == null)
|
||||
selected = pos;
|
||||
}
|
||||
} else {
|
||||
if (identity.id.equals(iid)) {
|
||||
selected = pos;
|
||||
break;
|
||||
}
|
||||
if (selected == null)
|
||||
selected = pos;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +191,7 @@ public class FragmentDialogIdentity extends FragmentDialogBase {
|
|||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
}
|
||||
}.execute(this, new Bundle(), "identity:select");
|
||||
}.execute(this, getArguments(), "identity:select");
|
||||
|
||||
return new AlertDialog.Builder(context)
|
||||
.setView(dview)
|
||||
|
@ -200,9 +211,10 @@ public class FragmentDialogIdentity extends FragmentDialogBase {
|
|||
.create();
|
||||
}
|
||||
|
||||
static void onCompose(Context context, LifecycleOwner owner, FragmentManager manager, FloatingActionButton fabCompose, long account) {
|
||||
static void onCompose(Context context, LifecycleOwner owner, FragmentManager manager, FloatingActionButton fabCompose, long account, long folder) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("account", account);
|
||||
args.putLong("folder", folder);
|
||||
|
||||
new SimpleTask<Boolean>() {
|
||||
@Override
|
||||
|
@ -217,12 +229,22 @@ public class FragmentDialogIdentity extends FragmentDialogBase {
|
|||
|
||||
@Override
|
||||
protected Boolean onExecute(Context context, Bundle args) {
|
||||
DB db = DB.getInstance(context);
|
||||
|
||||
long folder = args.getLong("folder");
|
||||
if (folder >= 0) {
|
||||
List<TupleIdentityCount> counts = db.message().getIdentitiesByFolder(folder);
|
||||
if (counts != null &&
|
||||
counts.size() == 1 &&
|
||||
counts.get(0).count >= MIN_IDENTITY_MESSAGES)
|
||||
args.putLong("identity", counts.get(0).identity);
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean identities_asked = prefs.getBoolean("identities_asked", false);
|
||||
if (identities_asked)
|
||||
return false;
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
List<TupleIdentityEx> identities = db.identity().getComposableIdentities(null);
|
||||
return (identities != null && identities.size() > 1);
|
||||
}
|
||||
|
@ -236,7 +258,8 @@ public class FragmentDialogIdentity extends FragmentDialogBase {
|
|||
} else
|
||||
context.startActivity(new Intent(context, ActivityCompose.class)
|
||||
.putExtra("action", "new")
|
||||
.putExtra("account", account));
|
||||
.putExtra("account", account)
|
||||
.putExtra("identity", args.getLong("identity", -1L)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -88,6 +88,12 @@ public class FragmentDialogMarkdown extends FragmentDialogBase {
|
|||
markdown = Helper.readStream(is);
|
||||
}
|
||||
|
||||
if ("CHANGELOG.md".equals(name)) {
|
||||
int pos = markdown.indexOf("<!-- truncate here -->");
|
||||
if (pos > 0)
|
||||
markdown = markdown.substring(0, pos);
|
||||
}
|
||||
|
||||
markdown = markdown
|
||||
.replace("/FAQ.md#FAQ", "/FAQ.md#faq")
|
||||
.replace("/FAQ.md#user-content-faq", "/FAQ.md#faq")
|
||||
|
|
|
@ -46,6 +46,7 @@ public class FragmentDialogOperationsDelete extends FragmentDialogBase {
|
|||
final CheckBox cbMove = dview.findViewById(R.id.cbMove);
|
||||
final CheckBox cbFlag = dview.findViewById(R.id.cbFlag);
|
||||
final CheckBox cbDelete = dview.findViewById(R.id.cbDelete);
|
||||
final CheckBox cbSend = dview.findViewById(R.id.cbSend);
|
||||
|
||||
return new AlertDialog.Builder(context)
|
||||
.setView(dview)
|
||||
|
@ -58,6 +59,7 @@ public class FragmentDialogOperationsDelete extends FragmentDialogBase {
|
|||
args.putBoolean("move", cbMove.isChecked());
|
||||
args.putBoolean("flag", cbFlag.isChecked());
|
||||
args.putBoolean("delete", cbDelete.isChecked());
|
||||
args.putBoolean("send", cbSend.isChecked());
|
||||
|
||||
new SimpleTask<Integer>() {
|
||||
private Toast toast = null;
|
||||
|
@ -81,6 +83,7 @@ public class FragmentDialogOperationsDelete extends FragmentDialogBase {
|
|||
boolean move = args.getBoolean("move");
|
||||
boolean flag = args.getBoolean("flag");
|
||||
boolean delete = args.getBoolean("delete");
|
||||
boolean send = args.getBoolean("send");
|
||||
|
||||
int deleted = 0;
|
||||
DB db = DB.getInstance(context);
|
||||
|
@ -143,6 +146,14 @@ public class FragmentDialogOperationsDelete extends FragmentDialogBase {
|
|||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (send) {
|
||||
List<EntityOperation> ops = db.operation().getOperations(EntityOperation.SEND);
|
||||
for (EntityOperation op : ops) {
|
||||
ActivityCompose.undoSend(op.message, context);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted > 0)
|
||||
ServiceSynchronize.reload(context, null, true, "deleted operations");
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ public class FragmentDialogPermissions extends FragmentDialogBase {
|
|||
TextView tvContactPermissions = dview.findViewById(R.id.tvContactPermissions);
|
||||
TextView tvNotificationPermissions = dview.findViewById(R.id.tvNotificationPermissions);
|
||||
TextView tvDozeDevice = dview.findViewById(R.id.tvDozeDevice);
|
||||
TextView tvDozeAndroid15 = dview.findViewById(R.id.tvDozeAndroid15);
|
||||
TextView tvDozeAndroid12 = dview.findViewById(R.id.tvDozeAndroid12);
|
||||
CheckBox cbNotAgain = dview.findViewById(R.id.cbNotAgain);
|
||||
Group grp2 = dview.findViewById(R.id.grp2);
|
||||
|
@ -82,6 +83,7 @@ public class FragmentDialogPermissions extends FragmentDialogBase {
|
|||
tvContactPermissions.setVisibility(hasContactPermissions ? View.GONE : View.VISIBLE);
|
||||
tvNotificationPermissions.setVisibility(hasNotificationPermissions ? View.GONE : View.VISIBLE);
|
||||
tvDozeDevice.setVisibility(Helper.isKilling() && !isIgnoring ? View.VISIBLE : View.GONE);
|
||||
tvDozeAndroid15.setVisibility(Helper.isAndroid15() && !isIgnoring ? View.VISIBLE : View.GONE);
|
||||
tvDozeAndroid12.setVisibility(!canScheduleExact && !isIgnoring ? View.VISIBLE : View.GONE);
|
||||
|
||||
grp2.setVisibility(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue