mirror of https://github.com/M66B/FairEmail.git
Compare commits
456 Commits
Author | SHA1 | Date |
---|---|---|
M66B | 6d69e390c9 | |
M66B | 7171455db7 | |
M66B | 0934759a77 | |
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 | |
M66B | 58199e930f | |
M66B | b97ae1882c | |
M66B | 9ce09e3e72 | |
M66B | bf9eb91959 | |
M66B | 99145fd0d9 | |
M66B | d1935ae5a9 | |
M66B | 43d3f432f2 | |
M66B | 34efdd46ad | |
M66B | ec888beb85 | |
M66B | 145b5c9214 | |
M66B | ee690add25 | |
M66B | 0a107467ec | |
M66B | 9b8638f3f4 | |
M66B | 9bac559ee0 | |
M66B | 7cbad6c1b4 | |
M66B | f547423b8b | |
M66B | eae8e33fad | |
M66B | bd60de816c | |
M66B | 785387955f | |
M66B | e2f3da5629 | |
M66B | 31401f9fd2 | |
M66B | 538a630c31 | |
M66B | 710fd04781 | |
M66B | 441812c561 | |
M66B | 55779ac915 | |
M66B | ac55dcc31b | |
M66B | 613f72e769 | |
M66B | d337763a77 | |
M66B | 726ea077ff | |
M66B | 9150746f75 | |
M66B | 7d0da8d5fa | |
M66B | 9b656434bd | |
M66B | 27e955a13f | |
M66B | 96be6d30a1 | |
M66B | 760da17971 | |
M66B | a9f87270ce | |
M66B | 5d64c94dce | |
M66B | 7ca74f1899 | |
M66B | 57c6b804f5 | |
M66B | 435b3d75c8 | |
M66B | a9db609a80 | |
M66B | ae8f03e0cd | |
M66B | 80692be12d | |
M66B | 6fef2235fd | |
M66B | 2baa11161d | |
M66B | c72a5194c2 | |
M66B | 0181493b4a | |
M66B | 27ddf4f6e0 | |
M66B | ccd32b15ca | |
M66B | 6ce4c32bbe | |
M66B | 6c52d14966 | |
M66B | 704a994c7d | |
M66B | 6422b7924f | |
M66B | ee9b5aa901 | |
M66B | ef0c01e84e | |
M66B | 0f63effad1 | |
M66B | 43d6821520 | |
M66B | bc76f5b391 | |
M66B | 9b317defe5 | |
M66B | c5099952bb | |
M66B | fb30aa9fe0 | |
M66B | c3e088048b | |
M66B | 5830ce1461 | |
M66B | 9653454018 | |
M66B | 8dbf37884f | |
M66B | 3e15d628e7 | |
M66B | 71f1dd4d5f | |
M66B | 04244d2832 | |
M66B | 87d6a507ce | |
M66B | 64ad1c90dc | |
M66B | cfc2e27270 | |
M66B | 03006bbadb | |
M66B | 04106dd5b2 | |
Marcel Bokhorst | d34f5035ee | |
M66B | 9a1e4cd904 | |
M66B | 4e0e8a544a | |
M66B | 5c4152afad | |
M66B | fa4a02bc6e | |
Marcel Bokhorst | 70d66232ac | |
Yurt Page | 76e38e9f5c | |
M66B | fe2e8dc794 | |
M66B | a278433b13 | |
M66B | 8558f2f123 | |
M66B | 0577fed9ab | |
M66B | a431a0c00a | |
M66B | 08b29f9473 | |
M66B | b1dfcdfd59 | |
M66B | 6e9a1b8a18 | |
M66B | 37d466f4b6 | |
M66B | 2421f5eaee | |
M66B | 4653b104e0 | |
M66B | b45eb556d7 | |
M66B | 129b0dcaf9 | |
M66B | c8e91ea24a | |
M66B | 7286b628d1 | |
M66B | 17a3c538de | |
M66B | 86cc0c4649 | |
M66B | 9cf52b72a7 | |
M66B | bb0b97b2fc | |
M66B | ac59cb23b4 | |
M66B | c8e5cccea3 | |
M66B | c8083ca5fa | |
M66B | 637dec7f7e | |
M66B | 76ab574055 | |
M66B | cca1d13237 | |
M66B | 9fd01d48f2 | |
M66B | e45b90d9a7 | |
M66B | 589f222481 | |
M66B | 6470b22c71 | |
M66B | 98aea61a37 | |
M66B | ce4b044be1 | |
M66B | 6535467df1 | |
M66B | 5d641d48e2 | |
M66B | c0a0fef08c | |
M66B | b141489e61 | |
M66B | ab072056be | |
M66B | d77057a7a7 | |
M66B | 78384293d3 | |
M66B | b2859a06d4 | |
M66B | 1b18ce2066 | |
M66B | ac5830d111 | |
M66B | 3667bc468e | |
M66B | ce17db1e83 | |
M66B | d0bbc63198 | |
M66B | c120bcb4ae | |
M66B | b385a52cbf | |
M66B | 1a2ac920c5 | |
M66B | 8746e5362f | |
M66B | ef2578bd83 | |
M66B | 02ccd0d93a | |
M66B | 81a5ebfd02 | |
M66B | e3cc93528a | |
M66B | 188d1ec811 | |
M66B | a3da6c711e | |
M66B | c038d8a182 | |
M66B | 2f9b14a6da | |
M66B | d7043c13f2 | |
M66B | 8a4226174f | |
M66B | e3c215b2e1 | |
M66B | dbbc7111e9 | |
M66B | 0ba95456d9 | |
M66B | f1215287e4 | |
M66B | 0c5c960588 | |
M66B | 8d21c68afc | |
M66B | b61ea3303b |
|
@ -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>
|
|
@ -57,3 +57,4 @@ FairEmail uses parts or all of:
|
|||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
||||
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
|
||||
|
|
133
CHANGELOG.md
133
CHANGELOG.md
|
@ -4,9 +4,136 @@
|
|||
|
||||
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.2169 - 2024-03-16 *
|
||||
### 1.2176 - 2024-04-22 *
|
||||
|
||||
* Fixed British English translation
|
||||
* Small improvements and minor bug fixes
|
||||
|
||||
### 1.2175 - 2024-04-20
|
||||
|
||||
* Fixed primary inbox navigation
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2174 - 2024-04-19
|
||||
|
||||
* Added expression conditions to rules, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2173 - 2024-04-16
|
||||
|
||||
* Added *primary inbox* start screen option
|
||||
* Added *NOT* option to rule conditions
|
||||
* 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.2172 - 2024-04-08
|
||||
|
||||
* Improved handling of messages via email forwarders (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sub>(*) Currently supported email forwarders:</sub>
|
||||
|
||||
* [addy.io](https://addy.io/)
|
||||
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
|
||||
* [Firefox Relay](https://relay.firefox.com/)
|
||||
* [SimpleLogin](https://simplelogin.io/)
|
||||
|
||||
### 1.2171 - 2024-03-30
|
||||
|
||||
* Added [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
|
||||
* Added answer button to buttons configuration
|
||||
* 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.2170 - 2024-03-23
|
||||
|
||||
* Added Arabic to [DeepL translation](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq167) targets
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated build tools
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2169 - 2024-03-16
|
||||
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
@ -175,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)
|
||||
|
@ -1585,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
|
||||
|
||||
|
|
158
FAQ.md
158
FAQ.md
|
@ -415,6 +415,7 @@ Anything on this list is in random order and *might* be added in the near future
|
|||
* [(201) What is certificate transparency?](#faq201)
|
||||
* [(202) What is DNSSEC and what is DANE?](#faq202)
|
||||
* [(203) Where is my sent message?](#faq203)
|
||||
* [(204) How do I use Gemini?](#faq204)
|
||||
|
||||
[I have another question.](#get-support)
|
||||
|
||||
|
@ -549,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).
|
||||
|
@ -708,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*.
|
||||
|
@ -911,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 />
|
||||
|
||||
|
@ -1076,6 +1082,7 @@ In case the certificate chain is incorrect, you can tap on the little info butto
|
|||
After the certificate details the issuer or "selfSign" is shown.
|
||||
A certificate is self signed when the subject and the issuer are the same.
|
||||
Certificates from a certificate authority (CA) are marked with "[keyCertSign](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)".
|
||||
You can find the description of other key usage bits, like *cRLSign*, via this same link.
|
||||
Certificates found in the Android key store are marked with "Android".
|
||||
|
||||
A valid chain looks like this:
|
||||
|
@ -1095,8 +1102,6 @@ If you are looking for a free (test) S/MIME certificate, see [here](http://kb.mo
|
|||
Please be sure to [read this first](https://davidroessli.com/logs/2019/09/free-smime-certificates-in-2019/#update20191219)
|
||||
if you want to request an S/MIME Actalis certificate.
|
||||
|
||||
S/MIME certificates can for example be purchased via [Xolphin](https://www.xolphin.com/).
|
||||
|
||||
How to extract a public key from a S/MIME certificate:
|
||||
|
||||
```
|
||||
|
@ -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)
|
||||
|
@ -1707,6 +1718,7 @@ You can disable this feature in the advanced account settings.
|
|||
When a menu item to select/open/save a file is disabled (dimmed) or when you get the message *Storage access framework not available*,
|
||||
the [storage access framework](https://developer.android.com/guide/topics/providers/document-provider), a standard Android component, is probably not present.
|
||||
This might be because your custom ROM does not include it or because it was actively removed (debloated).
|
||||
Note that this will result in similar problems in other apps too.
|
||||
|
||||
FairEmail does not request storage permissions, so this framework is required to select files and folders.
|
||||
No app, except maybe file managers, targeting Android 4.4 KitKat or later should ask for storage permissions because it would allow access to *all* files.
|
||||
|
@ -1745,15 +1757,15 @@ please [contact me](https://contact.faircode.eu/?product=fairemailsupport).
|
|||
|
||||
External image:
|
||||
|
||||
<img alt="External image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_image_black_48dp.png" width="48" height="48" />
|
||||
<img alt="External image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_image_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Embedded image:
|
||||
|
||||
<img alt="Embedded image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_photo_library_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Embedded image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_photo_library_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Broken image:
|
||||
|
||||
<img alt="Broken image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_broken_image_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Broken image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_broken_image_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Note that downloading external images from a remote server can be used to record you did see a message, which you likely don't want if the message is spam or malicious.
|
||||
|
||||
|
@ -1865,6 +1877,8 @@ Note that this is independent of receiving messages.
|
|||
|
||||
🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23faq34)
|
||||
|
||||
Matched identities are used to select the correct (matched) identity when replying to a message.
|
||||
|
||||
Identities are as expected matched by account.
|
||||
For incoming messages the *to*, *cc*, *bcc*, *from* and *(X-)delivered/envelope/original-to* addresses will be checked (in this order)
|
||||
and for outgoing messages (drafts, outbox and sent) only the *from* addresses will be checked.
|
||||
|
@ -2381,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>
|
||||
|
@ -2424,11 +2440,11 @@ so it is better to resize images with an image editor first.
|
|||
|
||||
The email icon in the folder list can be open (outlined) or closed (solid):
|
||||
|
||||
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_mail_outline_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Mail outline image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_mail_outline_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Message bodies and attachments are not downloaded by default.
|
||||
|
||||
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_email_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Mail image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_email_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Message bodies and attachments are downloaded by default.
|
||||
|
||||
|
@ -2689,6 +2705,10 @@ If a rule is part of a group, stop processing means stop processing the group.
|
|||
|
||||
Since version 1.2018 there is a rule option to run rules daily on messages (around 1:00am) older than xxx.
|
||||
|
||||
<br>
|
||||
|
||||
**Conditions**
|
||||
|
||||
The following rule conditions are available:
|
||||
|
||||
* Sender (from, reply-to) contains or sender is contact
|
||||
|
@ -2699,9 +2719,11 @@ The following rule conditions are available:
|
|||
* Text contains (since version 1.1785)
|
||||
* Absolute time (received) between (since version 1.1540)
|
||||
* Relative time (received) between
|
||||
* Expression (since version 1.2174)
|
||||
|
||||
All the conditions of a rule need to be true for the rule action to be executed.
|
||||
All conditions are optional, but there needs to be at least one condition, to prevent matching all messages.
|
||||
|
||||
If you want to match all senders or all recipients, you can just use the @ character as condition because all email addresses will contain this character.
|
||||
If you want to match a domain name, you can use as a condition something like *@example.org*
|
||||
|
||||
|
@ -2724,6 +2746,7 @@ jsoup:html > body > div > a[href=https://example.org]
|
|||
```
|
||||
|
||||
You can use multiple rules, possibly with a *stop processing*, for an *or* or a *not* condition.
|
||||
Since version 1.2173 there is a *NOT* option for conditions that accept a regex.
|
||||
|
||||
Matching is not case sensitive, unless you use [regular expressions](https://en.wikipedia.org/wiki/Regular_expression).
|
||||
Please see [here](https://developer.android.com/reference/java/util/regex/Pattern) for the documentation of Java regular expressions.
|
||||
|
@ -2739,6 +2762,49 @@ 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:
|
||||
|
||||
* *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* (string/array of strings contains substring)
|
||||
* *matches* (string/array of strings matches regex)
|
||||
|
||||
The following extra functions are available:
|
||||
|
||||
* *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 conditions:
|
||||
|
||||
```header("X-Mailer") contains "Open-Xchange" && from matches ".*service@.*"```
|
||||
|
||||
```!onBlocklist() && hasMx() && attachments() > 0```
|
||||
|
||||
<br>
|
||||
|
||||
**Actions**
|
||||
|
||||
You can select one of these actions to apply to matching messages:
|
||||
|
||||
* No action (useful for *not*)
|
||||
|
@ -3160,7 +3226,7 @@ The BBC article '[Spy pixels in emails have become endemic](https://www.bbc.com/
|
|||
|
||||
FairEmail will in most cases automatically recognize tracking images and replace them by this icon:
|
||||
|
||||
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_my_location_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Tracking image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_my_location_black_48dp.png" width="48" height="48" />
|
||||
|
||||
Automatic recognition of tracking images can be disabled in the privacy settings.
|
||||
|
||||
|
@ -4039,6 +4105,12 @@ Composing messages using [Markdown](https://en.wikipedia.org/wiki/Markdown) can
|
|||
|
||||
<br />
|
||||
|
||||
*Rule expression condition (1.2174+)*
|
||||
|
||||
See [this FAQ](#faq71)
|
||||
|
||||
<br />
|
||||
|
||||
<a name="faq126"></a>
|
||||
**(126) Can message previews be sent to my smartwatch?**
|
||||
|
||||
|
@ -4415,7 +4487,7 @@ Note that trashing a message will permanently remove it from the server and that
|
|||
|
||||
To record voice notes you can press this icon in the bottom action bar of the message composer:
|
||||
|
||||
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_record_voice_over_black_48dp.png" width="48" height="48" />
|
||||
<img alt="Record image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_record_voice_over_black_48dp.png" width="48" height="48" />
|
||||
|
||||
This requires a compatible audio recorder app to be installed.
|
||||
In particular [this common intent](https://developer.android.com/reference/android/provider/MediaStore.Audio.Media.html#RECORD_SOUND_ACTION)
|
||||
|
@ -4496,7 +4568,7 @@ You likely came here because you are using a third party build of FairEmail.
|
|||
There is **only support** on the latest Play store version, the latest GitHub release and
|
||||
the F-Droid build, but **only if** the version number of the F-Droid build is the same as the version number of the latest GitHub release.
|
||||
|
||||
F-Droid builds irregularly, which can be problematic when there is an important update.
|
||||
F-Droid builds irregularly, which can be problematic if there is an important update.
|
||||
Therefore you are advised to switch to the GitHub release.
|
||||
|
||||
Note that developers have no control over F-Droid builds and the F-Droid infrastructure (apps, forums, etc.).
|
||||
|
@ -4625,7 +4697,7 @@ You might need to change [the Gmail IMAP settings](https://mail.google.com/mail/
|
|||
* When I mark a message in IMAP as deleted: Auto-Expunge off - Wait for the client to update the server.
|
||||
* When a message is marked as deleted and expunged from the last visible IMAP folder: Immediately delete the message forever
|
||||
|
||||
<img alt="External image" src="https://github.com/M66B/FairEmail/blob/master/images/Gmail_IMAP_delete_settings.png" width="600" height="333" />
|
||||
<img alt="External image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/Gmail_IMAP_delete_settings.png" width="600" height="333" />
|
||||
|
||||
Note that archived messages can be deleted only by moving them to the trash folder first.
|
||||
|
||||
|
@ -5502,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)
|
||||
|
@ -5513,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).
|
||||
|
@ -5523,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/)
|
||||
|
@ -5558,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>
|
||||
|
||||
|
@ -5803,6 +5888,33 @@ Basically, an outgoing message is either in the draft messages folder, the outbo
|
|||
|
||||
<br>
|
||||
|
||||
<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)
|
||||
1. Get an API key via [here](https://ai.google.dev/tutorials/setup)
|
||||
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 requires version 1.2171 or later for the GitHub version and version 1.2182 or later for the Play Store version.
|
||||
|
||||
<br>
|
||||
|
||||
<h2><a name="get-support"></a>Get support</h2>
|
||||
|
||||
🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23get-support)
|
||||
|
|
22
FAQ.yaml
22
FAQ.yaml
|
@ -3,5 +3,27 @@ header-includes: |
|
|||
<link rel="shortcut icon" href="https://raw.githubusercontent.com/M66B/FairEmail/master/app/src/main/ic_launcher-web.png">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
color: #212121;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
a:link { color: #C68400; }
|
||||
a:visited { color: #ff6f00; }
|
||||
a:hover { color: #039BE5; }
|
||||
a:active { color: #039BE5; }
|
||||
img { filter: invert(0%); }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
color: #FFFFFF;
|
||||
background: #424242;
|
||||
}
|
||||
a:link { color: #FFB300; }
|
||||
a:visited { color: #FF6F00; }
|
||||
a:hover { color: #01579B; }
|
||||
a:active { color: #01579B; }
|
||||
img { filter: invert(75%); }
|
||||
}
|
||||
</style>
|
||||
---
|
||||
|
|
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
|
||||
|
|
120
app/build.gradle
120
app/build.gradle
|
@ -3,9 +3,9 @@ apply plugin: 'com.bugsnag.android.gradle'
|
|||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
def getVersionCode = { -> return 2169 }
|
||||
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
|
||||
|
@ -65,10 +67,10 @@ android {
|
|||
|
||||
sourceSets {
|
||||
github {
|
||||
java.srcDirs = ['src/main/java', 'src/play/java', 'src/extra/java']
|
||||
java.srcDirs = ['src/main/java', 'src/github/java', 'src/extra/java']
|
||||
}
|
||||
large {
|
||||
java.srcDirs = ['src/main/java', 'src/play/java', 'src/extra/java']
|
||||
java.srcDirs = ['src/main/java', 'src/github/java', 'src/extra/java']
|
||||
}
|
||||
fdroid {
|
||||
java.srcDirs = ['src/main/java', 'src/fdroid/java', 'src/extra/java']
|
||||
|
@ -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 {
|
||||
|
@ -213,6 +219,10 @@ android {
|
|||
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
|
||||
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", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
large {
|
||||
|
@ -235,6 +245,10 @@ android {
|
|||
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
|
||||
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", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
fdroid {
|
||||
|
@ -266,6 +280,10 @@ android {
|
|||
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
|
||||
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", "\"https://f-droid.org/packages/%s/\""
|
||||
}
|
||||
play {
|
||||
|
@ -287,8 +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", "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)
|
||||
}
|
||||
|
@ -313,6 +335,10 @@ android {
|
|||
buildConfigField "String", "CLOUD_EMAIL", "\"\""
|
||||
buildConfigField "String", "OPENAI_ENDPOINT", "\"\""
|
||||
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", "\"\""
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +421,7 @@ preBuild.dependsOn copyChangelog
|
|||
|
||||
tasks.register('updateFAQ', Exec) {
|
||||
workingDir "${rootDir}"
|
||||
commandLine 'sh', '-c', 'pandoc --standalone --metadata-file FAQ.yaml FAQ.md -o index.html'
|
||||
commandLine 'sh', '-c', 'pandoc --standalone --columns=10000 -M document-css=false --metadata-file FAQ.yaml FAQ.md -o index.html'
|
||||
}
|
||||
|
||||
tasks.register('updatePrivacy', Exec) {
|
||||
|
@ -451,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"
|
||||
}
|
||||
}
|
||||
|
@ -517,51 +543,51 @@ dependencies {
|
|||
|
||||
def desugar_version = "2.0.4"
|
||||
def startup_version = "1.2.0-alpha02"
|
||||
def annotation_version_experimental = "1.4.0"
|
||||
def core_version = "1.12.0" // 1.13.0-alpha04
|
||||
def appcompat_version = "1.6.1" // 1.7.0-alpha03
|
||||
def annotation_version_experimental = "1.4.1"
|
||||
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.8.2" // 1.9.0-alpha03
|
||||
def fragment_version = "1.6.2" // 1.7.0-alpha10
|
||||
def windows_version = "1.2.0" // 1.3.0-alpha03
|
||||
def webkit_version = "1.10.0" // 1.11.0-alpha02
|
||||
def activity_version = " 1.9.0"
|
||||
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-alpha02
|
||||
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-alpha04
|
||||
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-alpha01
|
||||
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.1.0
|
||||
def playservicesbase_version = "18.3.0";
|
||||
def billingclient_version = "6.0.1" // 6.2.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"
|
||||
|
@ -571,12 +597,13 @@ 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"
|
||||
def evalex_version = "3.2.0"
|
||||
|
||||
// https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
|
||||
|
@ -677,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"
|
||||
|
@ -688,14 +716,12 @@ dependencies {
|
|||
|
||||
// https://developer.android.com/google/play/billing/billing_library_releases_notes
|
||||
// https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.html
|
||||
githubImplementation "com.android.billingclient:billing:$billingclient_version"
|
||||
largeImplementation "com.android.billingclient:billing:$billingclient_version"
|
||||
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
|
||||
|
@ -780,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
|
||||
|
@ -849,4 +876,7 @@ dependencies {
|
|||
// https://github.com/zxing/zxing
|
||||
// https://mvnrepository.com/artifact/com.google.zxing/core
|
||||
implementation "com.google.zxing:core:$zxing_version"
|
||||
|
||||
// https://github.com/ezylang/EvalEx
|
||||
implementation "com.ezylang:EvalEx:$evalex_version"
|
||||
}
|
||||
|
|
|
@ -161,3 +161,6 @@
|
|||
-dontwarn java.lang.**
|
||||
-dontwarn javax.naming.**
|
||||
-dontwarn sun.reflect.Reflection
|
||||
|
||||
#EvalEx
|
||||
-dontwarn lombok.Generated
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -131,6 +131,15 @@
|
|||
android:theme="@style/Theme.AppCompat.TranslucentSplash">
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_performance_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -622,6 +631,7 @@
|
|||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.TEMPLATE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
<action android:name="${applicationId}.ADGUARD" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -139,6 +139,15 @@
|
|||
android:theme="@style/Theme.AppCompat.TranslucentSplash">
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_performance_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -629,6 +638,7 @@
|
|||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.TEMPLATE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
<action android:name="${applicationId}.ADGUARD" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -366,6 +366,9 @@ public class Bimi {
|
|||
}
|
||||
}
|
||||
|
||||
if (bitmap != null && !verified)
|
||||
Log.i("BIMI unverified");
|
||||
|
||||
return (bitmap == null ? null : new Pair<>(bitmap, verified));
|
||||
}
|
||||
|
||||
|
|
|
@ -138,6 +138,15 @@
|
|||
android:theme="@style/Theme.AppCompat.TranslucentSplash">
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_performance_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -628,6 +637,7 @@
|
|||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.TEMPLATE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
<action android:name="${applicationId}.ADGUARD" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -98,25 +98,72 @@ 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);
|
||||
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
}
|
||||
|
||||
if (Helper.isPlayStoreInstall() || isTesting(this)) {
|
||||
Log.i("IAB start");
|
||||
if (Helper.isPlayStoreInstall() || isTesting(this))
|
||||
try {
|
||||
Log.i("IAB start");
|
||||
/*
|
||||
billingClient = BillingClient.newBuilder(getApplicationContext()
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
billingClient.startConnection(this);
|
||||
billingClient = BillingClient.newBuilder(getApplicationContext())
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
billingClient.startConnection(this);
|
||||
getLifecycle().addObserver(new LifecycleObserver() {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
public void onDestroyed() {
|
||||
getLifecycle().removeObserver(this);
|
||||
if (billingClient != null)
|
||||
try {
|
||||
Log.i("IAB end");
|
||||
billingClient.endConnection();
|
||||
billingClient = null;
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
/*
|
||||
Exception java.lang.RuntimeException:
|
||||
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4171)
|
||||
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:4317)
|
||||
at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:101)
|
||||
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
|
||||
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
|
||||
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2576)
|
||||
at android.os.Handler.dispatchMessage (Handler.java:106)
|
||||
at android.os.Looper.loopOnce (Looper.java:226)
|
||||
at android.os.Looper.loop (Looper.java:313)
|
||||
at android.app.ActivityThread.main (ActivityThread.java:8772)
|
||||
at java.lang.reflect.Method.invoke (Method.java)
|
||||
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
|
||||
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
|
||||
Caused by java.lang.IllegalStateException: Too many bind requests(999+) for service Intent { act=com.android.vending.billing.InAppBillingService.BIND pkg=com.android.vending cmp=com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService (has extras) }
|
||||
at android.app.ContextImpl.bindServiceCommon (ContextImpl.java:2115)
|
||||
at android.app.ContextImpl.bindService (ContextImpl.java:2024)
|
||||
at android.content.ContextWrapper.bindService (ContextWrapper.java:870)
|
||||
at com.android.billingclient.api.BillingClientImpl.startConnection (com.android.billingclient:billing@@4.1.0:52)
|
||||
at eu.faircode.email.ActivityBilling.onCreate (ActivityBilling.java:116)
|
||||
at eu.faircode.email.ActivityView.onCreate (ActivityView.java:192)
|
||||
at android.app.Activity.performCreate (Activity.java:8565)
|
||||
at android.app.Activity.performCreate (Activity.java:8544)
|
||||
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1384)
|
||||
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4152)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -152,14 +199,6 @@ public class ActivityBilling extends ActivityBase implements
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
//if (billingClient != null)
|
||||
// billingClient.endConnection();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static String getSkuPro(Context context) {
|
||||
if (isTesting(context))
|
||||
|
@ -176,7 +215,7 @@ public class ActivityBilling extends ActivityBase implements
|
|||
prefs.getBoolean("test_iab", false));
|
||||
}
|
||||
|
||||
private static String getChallenge(Context context) throws NoSuchAlgorithmException {
|
||||
static String getChallenge(Context context) throws NoSuchAlgorithmException {
|
||||
String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
if (android_id == null) {
|
||||
Log.e("Android ID empty");
|
||||
|
@ -334,6 +373,8 @@ public class ActivityBilling extends ActivityBase implements
|
|||
@Override
|
||||
public void delegate() {
|
||||
try {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
boolean ready = billingClient.isReady();
|
||||
Log.i("IAB ready=" + ready);
|
||||
if (!ready)
|
||||
|
@ -450,6 +491,8 @@ public class ActivityBilling extends ActivityBase implements
|
|||
if (isPurchaseValid(purchase)) {
|
||||
editor.putBoolean("pro", true);
|
||||
editor.putLong(sku + ".cached", new Date().getTime());
|
||||
editor.putString("iab_json", purchase.getOriginalJson());
|
||||
editor.putString("iab_signature", purchase.getSignature());
|
||||
}
|
||||
|
||||
if (!purchase.isAcknowledged())
|
||||
|
@ -643,6 +686,10 @@ public class ActivityBilling extends ActivityBase implements
|
|||
// User pressed back or canceled a dialog
|
||||
return "USER_CANCELED";
|
||||
|
||||
case BillingClient.BillingResponseCode.NETWORK_ERROR:
|
||||
// A network error occurred during the operation
|
||||
return "NETWORK_ERROR";
|
||||
|
||||
default:
|
||||
return Integer.toString(result.getResponseCode());
|
||||
}
|
||||
|
|
|
@ -138,6 +138,15 @@
|
|||
android:theme="@style/Theme.AppCompat.TranslucentSplash">
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_performance_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -628,6 +637,7 @@
|
|||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.TEMPLATE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
<action android:name="${applicationId}.ADGUARD" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -0,0 +1,699 @@
|
|||
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.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
/*
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.ConsumeParams;
|
||||
import com.android.billingclient.api.ConsumeResponseListener;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.PurchasesResponseListener;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
*/
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class ActivityBilling extends ActivityBase implements
|
||||
/* BillingClientStateListener, SkuDetailsResponseListener, PurchasesResponseListener, PurchasesUpdatedListener, */
|
||||
FragmentManager.OnBackStackChangedListener {
|
||||
private boolean standalone = false;
|
||||
private int backoff = 4; // seconds
|
||||
//private BillingClient billingClient = null;
|
||||
private List<IBillingListener> listeners = new ArrayList<>();
|
||||
|
||||
static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
|
||||
static final String ACTION_PURCHASE_CONSUME = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CONSUME";
|
||||
static final String ACTION_PURCHASE_ERROR = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_ERROR";
|
||||
|
||||
private static final String SKU_TEST = "android.test.purchased";
|
||||
private static final long MAX_SKU_CACHE_DURATION = 24 * 3600 * 1000L; // milliseconds
|
||||
private static final long MAX_SKU_NOACK_DURATION = 24 * 3600 * 1000L; // milliseconds
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall")
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
onCreate(savedInstanceState, true);
|
||||
}
|
||||
|
||||
protected void onCreate(Bundle savedInstanceState, boolean standalone) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
this.standalone = standalone;
|
||||
|
||||
if (standalone) {
|
||||
setContentView(R.layout.activity_billing);
|
||||
|
||||
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);
|
||||
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
}
|
||||
|
||||
if (Helper.isPlayStoreInstall() || isTesting(this))
|
||||
try {
|
||||
Log.i("IAB start");
|
||||
/*
|
||||
billingClient = BillingClient.newBuilder(getApplicationContext())
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
billingClient.startConnection(this);
|
||||
getLifecycle().addObserver(new LifecycleObserver() {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
public void onDestroyed() {
|
||||
getLifecycle().removeObserver(this);
|
||||
if (billingClient != null)
|
||||
try {
|
||||
Log.i("IAB end");
|
||||
billingClient.endConnection();
|
||||
billingClient = null;
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
/*
|
||||
Exception java.lang.RuntimeException:
|
||||
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4171)
|
||||
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:4317)
|
||||
at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:101)
|
||||
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
|
||||
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
|
||||
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2576)
|
||||
at android.os.Handler.dispatchMessage (Handler.java:106)
|
||||
at android.os.Looper.loopOnce (Looper.java:226)
|
||||
at android.os.Looper.loop (Looper.java:313)
|
||||
at android.app.ActivityThread.main (ActivityThread.java:8772)
|
||||
at java.lang.reflect.Method.invoke (Method.java)
|
||||
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
|
||||
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
|
||||
Caused by java.lang.IllegalStateException: Too many bind requests(999+) for service Intent { act=com.android.vending.billing.InAppBillingService.BIND pkg=com.android.vending cmp=com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService (has extras) }
|
||||
at android.app.ContextImpl.bindServiceCommon (ContextImpl.java:2115)
|
||||
at android.app.ContextImpl.bindService (ContextImpl.java:2024)
|
||||
at android.content.ContextWrapper.bindService (ContextWrapper.java:870)
|
||||
at com.android.billingclient.api.BillingClientImpl.startConnection (com.android.billingclient:billing@@4.1.0:52)
|
||||
at eu.faircode.email.ActivityBilling.onCreate (ActivityBilling.java:116)
|
||||
at eu.faircode.email.ActivityView.onCreate (ActivityView.java:192)
|
||||
at android.app.Activity.performCreate (Activity.java:8565)
|
||||
at android.app.Activity.performCreate (Activity.java:8544)
|
||||
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1384)
|
||||
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4152)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackStackChanged() {
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (standalone) {
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
|
||||
IntentFilter iff = new IntentFilter();
|
||||
iff.addAction(ACTION_PURCHASE);
|
||||
iff.addAction(ACTION_PURCHASE_CONSUME);
|
||||
iff.addAction(ACTION_PURCHASE_ERROR);
|
||||
lbm.registerReceiver(receiver, iff);
|
||||
}
|
||||
|
||||
//if (billingClient != null && billingClient.isReady())
|
||||
// queryPurchases();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (standalone) {
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
|
||||
lbm.unregisterReceiver(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static String getSkuPro(Context context) {
|
||||
if (isTesting(context))
|
||||
return SKU_TEST;
|
||||
else
|
||||
return BuildConfig.APPLICATION_ID + ".pro";
|
||||
}
|
||||
|
||||
static boolean isTesting(Context context) {
|
||||
if (context == null)
|
||||
return false;
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return (BuildConfig.DEBUG && BuildConfig.TEST_RELEASE &&
|
||||
prefs.getBoolean("test_iab", false));
|
||||
}
|
||||
|
||||
static String getChallenge(Context context) throws NoSuchAlgorithmException {
|
||||
String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
if (android_id == null) {
|
||||
Log.e("Android ID empty");
|
||||
android_id = Long.toHexString(System.currentTimeMillis() / (24 * 3600 * 1000L));
|
||||
}
|
||||
return Helper.sha256(android_id);
|
||||
}
|
||||
|
||||
private static String getResponse(Context context) throws NoSuchAlgorithmException {
|
||||
return Helper.sha256(BuildConfig.APPLICATION_ID.replace(".debug", "") + getChallenge(context));
|
||||
}
|
||||
|
||||
static boolean activatePro(Context context, Uri data) throws NoSuchAlgorithmException {
|
||||
String response = data.getQueryParameter("response");
|
||||
return activatePro(context, response);
|
||||
}
|
||||
|
||||
static boolean activatePro(Context context, String response) throws NoSuchAlgorithmException {
|
||||
String challenge = getChallenge(context);
|
||||
Log.i("IAB challenge=" + challenge);
|
||||
Log.i("IAB response=" + response);
|
||||
String expected = getResponse(context);
|
||||
if (expected.equals(response)) {
|
||||
Log.i("IAB response valid");
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit()
|
||||
.putBoolean("pro", true)
|
||||
.putBoolean("play_store", false)
|
||||
.apply();
|
||||
|
||||
WidgetUnified.updateData(context);
|
||||
return true;
|
||||
} else {
|
||||
Log.i("IAB response invalid");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isPro(Context context) {
|
||||
if (BuildConfig.DEBUG && false)
|
||||
return true;
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("pro", false);
|
||||
}
|
||||
|
||||
private BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
|
||||
if (ACTION_PURCHASE.equals(intent.getAction()))
|
||||
onPurchase(intent);
|
||||
else if (ACTION_PURCHASE_CONSUME.equals(intent.getAction()))
|
||||
;//onPurchaseConsume(intent);
|
||||
else if (ACTION_PURCHASE_ERROR.equals(intent.getAction()))
|
||||
;//onPurchaseError(intent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void onPurchase(Intent intent) {
|
||||
if (Helper.isPlayStoreInstall() || isTesting(this)) {
|
||||
String skuPro = getSkuPro(this);
|
||||
Log.i("IAB purchase SKU=" + skuPro);
|
||||
/*
|
||||
SkuDetailsParams.Builder builder = SkuDetailsParams.newBuilder();
|
||||
builder.setSkusList(Arrays.asList(skuPro));
|
||||
builder.setType(BillingClient.SkuType.INAPP);
|
||||
billingClient.querySkuDetailsAsync(builder.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(@NonNull BillingResult r, List<SkuDetails> skuDetailsList) {
|
||||
if (r.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
if (skuDetailsList.size() == 0)
|
||||
reportError(null, "Unknown SKU=" + skuPro);
|
||||
else {
|
||||
SkuDetails skuDetails = skuDetailsList.get(0);
|
||||
Log.i("IAB purchase details=" + skuDetails);
|
||||
|
||||
BillingFlowParams.Builder flowParams = BillingFlowParams.newBuilder();
|
||||
flowParams.setSkuDetails(skuDetails);
|
||||
|
||||
BillingResult result = billingClient.launchBillingFlow(ActivityBilling.this, flowParams.build());
|
||||
if (result.getResponseCode() != BillingClient.BillingResponseCode.OK)
|
||||
reportError(result, "IAB launch billing flow");
|
||||
}
|
||||
} else
|
||||
reportError(r, "IAB query SKUs");
|
||||
}
|
||||
});
|
||||
*/
|
||||
} else
|
||||
try {
|
||||
Uri uri = Uri.parse(BuildConfig.PRO_FEATURES_URI +
|
||||
"?challenge=" + getChallenge(this) +
|
||||
"&version=" + BuildConfig.VERSION_CODE);
|
||||
Helper.view(this, uri, true);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, 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();
|
||||
|
||||
void onDisconnected();
|
||||
|
||||
void onSkuDetails(String sku, String price);
|
||||
|
||||
void onPurchasePending(String sku);
|
||||
|
||||
void onPurchased(String sku, boolean purchased);
|
||||
|
||||
void onError(String message);
|
||||
}
|
||||
|
||||
void addBillingListener(final IBillingListener listener, LifecycleOwner owner) {
|
||||
Log.i("IAB adding billing listener=" + listener);
|
||||
listeners.add(listener);
|
||||
|
||||
//if (billingClient != null)
|
||||
// if (billingClient.isReady()) {
|
||||
// listener.onConnected();
|
||||
// queryPurchases();
|
||||
// } else
|
||||
// listener.onDisconnected();
|
||||
|
||||
owner.getLifecycle().addObserver(new LifecycleObserver() {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
public void onDestroyed() {
|
||||
Log.i("IAB removing billing listener=" + listener);
|
||||
listeners.remove(listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
/*
|
||||
private void checkPurchases(List<Purchase> purchases) {
|
||||
Log.i("IAB purchases=" + (purchases == null ? null : purchases.size()));
|
||||
|
||||
List<String> query = new ArrayList<>();
|
||||
query.add(getSkuPro(this));
|
||||
|
||||
if (purchases != null) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
if (prefs.getBoolean("play_store", true)) {
|
||||
long cached = prefs.getLong(getSkuPro(this) + ".cached", 0);
|
||||
if (cached + MAX_SKU_CACHE_DURATION < new Date().getTime()) {
|
||||
Log.i("IAB cache expired=" + new Date(cached));
|
||||
editor.remove("pro");
|
||||
} else
|
||||
Log.i("IAB caching until=" + new Date(cached + MAX_SKU_CACHE_DURATION));
|
||||
}
|
||||
|
||||
for (Purchase purchase : purchases)
|
||||
for (String sku : purchase.getSkus())
|
||||
try {
|
||||
query.remove(sku);
|
||||
|
||||
long time = purchase.getPurchaseTime();
|
||||
Log.i("IAB SKU=" + sku +
|
||||
" purchased=" + isPurchased(purchase) +
|
||||
" valid=" + isPurchaseValid(purchase) +
|
||||
" time=" + new Date(time));
|
||||
Log.i("IAB json=" + purchase.getOriginalJson());
|
||||
|
||||
for (IBillingListener listener : listeners)
|
||||
if (isPurchaseValid(purchase))
|
||||
listener.onPurchased(sku, true);
|
||||
else
|
||||
listener.onPurchasePending(sku);
|
||||
|
||||
if (isPurchased(purchase)) {
|
||||
byte[] decodedKey = Base64.decode(getString(R.string.public_key), Base64.DEFAULT);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
||||
Signature sig = Signature.getInstance("SHA1withRSA");
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(purchase.getOriginalJson().getBytes());
|
||||
if (SKU_TEST.equals(sku) ||
|
||||
sig.verify(Base64.decode(purchase.getSignature(), Base64.DEFAULT))) {
|
||||
Log.i("IAB valid signature");
|
||||
if (getSkuPro(this).equals(sku)) {
|
||||
if (isPurchaseValid(purchase)) {
|
||||
editor.putBoolean("pro", true);
|
||||
editor.putLong(sku + ".cached", new Date().getTime());
|
||||
editor.putString("iab_json", purchase.getOriginalJson());
|
||||
editor.putString("iab_signature", purchase.getSignature());
|
||||
}
|
||||
|
||||
if (!purchase.isAcknowledged())
|
||||
acknowledgePurchase(purchase, 0);
|
||||
}
|
||||
} else {
|
||||
Log.w("IAB invalid signature");
|
||||
editor.putBoolean("pro", false);
|
||||
reportError(null, "Invalid purchase");
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
reportError(null, Log.formatThrowable(ex, false));
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
|
||||
WidgetUnified.updateData(this);
|
||||
}
|
||||
|
||||
if (query.size() > 0)
|
||||
querySkus(query);
|
||||
}
|
||||
|
||||
private void querySkus(List<String> query) {
|
||||
Log.i("IAB query SKUs");
|
||||
SkuDetailsParams.Builder builder = SkuDetailsParams.newBuilder();
|
||||
builder.setSkusList(query);
|
||||
builder.setType(BillingClient.SkuType.INAPP);
|
||||
billingClient.querySkuDetailsAsync(builder.build(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkuDetailsResponse(@NonNull BillingResult result, List<SkuDetails> skuDetailsList) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
for (SkuDetails skuDetail : skuDetailsList) {
|
||||
Log.i("IAB SKU detail=" + skuDetail);
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onSkuDetails(skuDetail.getSku(), skuDetail.getPrice());
|
||||
}
|
||||
} else
|
||||
reportError(result, "IAB query SKUs");
|
||||
}
|
||||
|
||||
private void consumePurchase(final Purchase purchase) {
|
||||
for (String sku : purchase.getSkus()) {
|
||||
Log.i("IAB consuming SKU=" + sku);
|
||||
ConsumeParams params = ConsumeParams.newBuilder()
|
||||
.setPurchaseToken(purchase.getPurchaseToken())
|
||||
.build();
|
||||
billingClient.consumeAsync(params, new ConsumeResponseListener() {
|
||||
@Override
|
||||
public void onConsumeResponse(@NonNull BillingResult result, @NonNull String purchaseToken) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onPurchased(sku, false);
|
||||
} else
|
||||
reportError(result, "IAB consuming SKU=" + sku);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void acknowledgePurchase(final Purchase purchase, int retry) {
|
||||
for (String sku : purchase.getSkus()) {
|
||||
Log.i("IAB acknowledging purchase SKU=" + sku);
|
||||
AcknowledgePurchaseParams params =
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.getPurchaseToken())
|
||||
.build();
|
||||
billingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() {
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(@NonNull BillingResult result) {
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivityBilling.this);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean("pro", true);
|
||||
editor.putLong(sku + ".cached", new Date().getTime());
|
||||
editor.apply();
|
||||
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onPurchased(sku, true);
|
||||
|
||||
WidgetUnified.updateData(ActivityBilling.this);
|
||||
} else {
|
||||
if (retry < 3) {
|
||||
new Handler().postDelayed(new RunnableEx("IAB ack retry") {
|
||||
@Override
|
||||
public void delegate() {
|
||||
acknowledgePurchase(purchase, retry + 1);
|
||||
}
|
||||
}, (retry + 1) * 10 * 1000L);
|
||||
} else
|
||||
reportError(result, "IAB acknowledged SKU=" + sku);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPurchased(Purchase purchase) {
|
||||
return (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED);
|
||||
}
|
||||
|
||||
private boolean isPurchaseValid(Purchase purchase) {
|
||||
return (isPurchased(purchase) &&
|
||||
(purchase.isAcknowledged() ||
|
||||
purchase.getSkus().contains(SKU_TEST) ||
|
||||
purchase.getPurchaseTime() + MAX_SKU_NOACK_DURATION > new Date().getTime()));
|
||||
}
|
||||
|
||||
private void reportError(BillingResult result, String stage) {
|
||||
String message;
|
||||
if (result == null)
|
||||
message = stage;
|
||||
else {
|
||||
message = getBillingResponseText(result);
|
||||
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE)
|
||||
message += " Is the Play Store app logged into the account used to install the app?";
|
||||
|
||||
String debug = result.getDebugMessage();
|
||||
if (!TextUtils.isEmpty(debug))
|
||||
message += " " + debug;
|
||||
|
||||
message += " " + stage;
|
||||
}
|
||||
|
||||
EntityLog.log(this, message);
|
||||
|
||||
if (result != null) {
|
||||
// https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse#service_disconnected
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
|
||||
retry(60);
|
||||
|
||||
if (result.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED)
|
||||
return;
|
||||
}
|
||||
|
||||
for (IBillingListener listener : listeners)
|
||||
listener.onError(message);
|
||||
}
|
||||
|
||||
private static String getBillingResponseText(BillingResult result) {
|
||||
switch (result.getResponseCode()) {
|
||||
case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
|
||||
// Billing API version is not supported for the type requested
|
||||
return "BILLING_UNAVAILABLE";
|
||||
|
||||
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
|
||||
// Invalid arguments provided to the API.
|
||||
return "DEVELOPER_ERROR";
|
||||
|
||||
case BillingClient.BillingResponseCode.ERROR:
|
||||
// Fatal error during the API action
|
||||
return "ERROR";
|
||||
|
||||
case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
|
||||
// Requested feature is not supported by Play Store on the current device.
|
||||
return "FEATURE_NOT_SUPPORTED";
|
||||
|
||||
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
|
||||
// Failure to purchase since item is already owned
|
||||
return "ITEM_ALREADY_OWNED";
|
||||
|
||||
case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
|
||||
// Failure to consume since item is not owned
|
||||
return "ITEM_NOT_OWNED";
|
||||
|
||||
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
|
||||
// Requested product is not available for purchase
|
||||
return "ITEM_UNAVAILABLE";
|
||||
|
||||
case BillingClient.BillingResponseCode.OK:
|
||||
// Success
|
||||
return "OK";
|
||||
|
||||
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
|
||||
// Play Store service is not connected now - potentially transient state.
|
||||
return "SERVICE_DISCONNECTED";
|
||||
|
||||
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
|
||||
// Network connection is down
|
||||
return "SERVICE_UNAVAILABLE";
|
||||
|
||||
case BillingClient.BillingResponseCode.SERVICE_TIMEOUT:
|
||||
// The request has reached the maximum timeout before Google Play responds.
|
||||
return "SERVICE_TIMEOUT";
|
||||
|
||||
case BillingClient.BillingResponseCode.USER_CANCELED:
|
||||
// User pressed back or canceled a dialog
|
||||
return "USER_CANCELED";
|
||||
|
||||
case BillingClient.BillingResponseCode.NETWORK_ERROR:
|
||||
// A network error occurred during the operation
|
||||
return "NETWORK_ERROR";
|
||||
|
||||
default:
|
||||
return Integer.toString(result.getResponseCode());
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
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.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ApplicationSecure extends ApplicationEx implements ProviderInstaller.ProviderInstallListener {
|
||||
private static final CountDownLatch lock = new CountDownLatch(1);
|
||||
|
||||
private static final long WAIT_INSTALLED = 750L; // milliseconds
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean ssl_update = prefs.getBoolean("ssl_update", true);
|
||||
if (ssl_update) {
|
||||
Log.i("Security provider check");
|
||||
ProviderInstaller.installIfNeededAsync(this, this);
|
||||
} else
|
||||
lock.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderInstalled() {
|
||||
Log.i("Security provider installed");
|
||||
lock.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderInstallFailed(int errorCode, Intent recoveryIntent) {
|
||||
Log.i("Security provider install failed" +
|
||||
" errorCode=" + errorCode +
|
||||
" recoveryIntent=" + recoveryIntent);
|
||||
lock.countDown();
|
||||
}
|
||||
|
||||
public static boolean waitProviderInstalled() {
|
||||
Log.i("Security provider wait");
|
||||
try {
|
||||
boolean succeeded = lock.await(WAIT_INSTALLED, TimeUnit.MILLISECONDS);
|
||||
Log.i("Security provider wait succeeded=" + succeeded);
|
||||
return succeeded;
|
||||
} catch (InterruptedException ex) {
|
||||
Log.i(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -131,6 +131,15 @@
|
|||
android:theme="@style/Theme.AppCompat.TranslucentSplash">
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
|
||||
<meta-data
|
||||
android:name="firebase_performance_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -623,6 +632,7 @@
|
|||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.TEMPLATE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
<action android:name="${applicationId}.ADGUARD" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -57,3 +57,4 @@ FairEmail uses parts or all of:
|
|||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
||||
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
|
||||
|
|
|
@ -4,9 +4,136 @@
|
|||
|
||||
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.2169 - 2024-03-16 *
|
||||
### 1.2176 - 2024-04-22 *
|
||||
|
||||
* Fixed British English translation
|
||||
* Small improvements and minor bug fixes
|
||||
|
||||
### 1.2175 - 2024-04-20
|
||||
|
||||
* Fixed primary inbox navigation
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2174 - 2024-04-19
|
||||
|
||||
* Added expression conditions to rules, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2173 - 2024-04-16
|
||||
|
||||
* Added *primary inbox* start screen option
|
||||
* Added *NOT* option to rule conditions
|
||||
* 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.2172 - 2024-04-08
|
||||
|
||||
* Improved handling of messages via email forwarders (*)
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
<sub>(*) Currently supported email forwarders:</sub>
|
||||
|
||||
* [addy.io](https://addy.io/)
|
||||
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
|
||||
* [Firefox Relay](https://relay.firefox.com/)
|
||||
* [SimpleLogin](https://simplelogin.io/)
|
||||
|
||||
### 1.2171 - 2024-03-30
|
||||
|
||||
* Added [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
|
||||
* Added answer button to buttons configuration
|
||||
* 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.2170 - 2024-03-23
|
||||
|
||||
* Added Arabic to [DeepL translation](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq167) targets
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated build tools
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2169 - 2024-03-16
|
||||
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
@ -175,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)
|
||||
|
@ -1585,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.
|
|
@ -1,4 +1,9 @@
|
|||
[
|
||||
{
|
||||
"language": "AR",
|
||||
"name": "Arabic",
|
||||
"supports_formality": false
|
||||
},
|
||||
{
|
||||
"language": "BG",
|
||||
"name": "Bulgarian",
|
||||
|
|
|
@ -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-06T15:14:58Z
|
||||
// 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
|
||||
|
||||
|
@ -6988,10 +6988,6 @@ auto
|
|||
// https://www.iana.org/domains/root/db/autos.html
|
||||
autos
|
||||
|
||||
// avianca : Avianca Inc.
|
||||
// https://www.iana.org/domains/root/db/avianca.html
|
||||
avianca
|
||||
|
||||
// aws : AWS Registry LLC
|
||||
// https://www.iana.org/domains/root/db/aws.html
|
||||
aws
|
||||
|
@ -11124,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
|
||||
|
||||
|
@ -11204,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
|
||||
|
@ -11350,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
|
||||
|
@ -11389,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
|
||||
|
@ -11414,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
|
||||
|
@ -11423,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
|
||||
|
@ -11444,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
|
||||
|
@ -11474,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
|
||||
|
@ -11485,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
|
||||
|
@ -11784,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
|
||||
|
@ -11823,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
|
||||
|
@ -11830,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
|
||||
|
@ -11844,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
|
||||
|
@ -11955,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
|
||||
|
@ -12080,6 +12131,7 @@ autocode.dev
|
|||
|
||||
// AVM : https://avm.de
|
||||
// Submitted by Andreas Weise <a.weise@avm.de>
|
||||
myfritz.link
|
||||
myfritz.net
|
||||
|
||||
// AVStack Pte. Ltd. : https://avstack.io
|
||||
|
@ -12147,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
|
||||
|
@ -12193,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
|
||||
|
@ -12310,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>
|
||||
|
@ -12357,6 +12417,12 @@ pages.dev
|
|||
r2.dev
|
||||
workers.dev
|
||||
|
||||
// cloudscale.ch AG : https://www.cloudscale.ch/
|
||||
// Submitted by Gaudenz Steinlin <support@cloudscale.ch>
|
||||
cust.cloudscale.ch
|
||||
objects.lpg.cloudscale.ch
|
||||
objects.rma.cloudscale.ch
|
||||
|
||||
// Clovyr : https://clovyr.io
|
||||
// Submitted by Patrick Nielsen <patrick@clovyr.io>
|
||||
wnext.app
|
||||
|
@ -12374,22 +12440,33 @@ co.cz
|
|||
|
||||
// CDN77.com : http://www.cdn77.com
|
||||
// Submitted by Jan Krpes <jan.krpes@cdn77.com>
|
||||
c.cdn77.org
|
||||
cdn77-storage.com
|
||||
rsc.contentproxy9.cz
|
||||
cdn77-ssl.net
|
||||
r.cdn77.net
|
||||
rsc.cdn77.org
|
||||
ssl.origin.cdn77-secure.org
|
||||
c.cdn77.org
|
||||
rsc.cdn77.org
|
||||
|
||||
// Cloud DNS Ltd : http://www.cloudns.net
|
||||
// Submitted by Aleksander Hristov <noc@cloudns.net>
|
||||
// Submitted by Aleksander Hristov <noc@cloudns.net> & Boyan Peychev <boyan@cloudns.net>
|
||||
cloudns.asia
|
||||
cloudns.be
|
||||
cloudns.biz
|
||||
cloudns.club
|
||||
cloudns.cc
|
||||
cloudns.ch
|
||||
cloudns.cl
|
||||
cloudns.club
|
||||
dnsabr.com
|
||||
cloudns.cx
|
||||
cloudns.eu
|
||||
cloudns.in
|
||||
cloudns.info
|
||||
dns-cloud.net
|
||||
dns-dynamic.net
|
||||
cloudns.nz
|
||||
cloudns.org
|
||||
cloudns.ph
|
||||
cloudns.pro
|
||||
cloudns.pw
|
||||
cloudns.us
|
||||
|
@ -12402,6 +12479,11 @@ cnpy.gdn
|
|||
// Submitted by Moritz Marquardt <git@momar.de>
|
||||
codeberg.page
|
||||
|
||||
// CodeSandbox B.V. : https://codesandbox.io
|
||||
// Submitted by Ives van Hoorne <abuse@codesandbox.io>
|
||||
csb.app
|
||||
preview.csb.app
|
||||
|
||||
// CoDNS B.V.
|
||||
co.nl
|
||||
co.no
|
||||
|
@ -12520,6 +12602,7 @@ dyndns.dappnode.io
|
|||
// Dark, Inc. : https://darklang.com
|
||||
// Submitted by Paul Biggar <ops@darklang.com>
|
||||
builtwithdark.com
|
||||
darklang.io
|
||||
|
||||
// DataDetect, LLC. : https://datadetect.com
|
||||
// Submitted by Andrew Banchich <abanchich@sceven.com>
|
||||
|
@ -12918,6 +13001,10 @@ ondigitalocean.app
|
|||
// Submitted by Robin H. Johnson <psl-maintainers@digitalocean.com>
|
||||
*.digitaloceanspaces.com
|
||||
|
||||
// DigitalPlat : https://www.digitalplat.org/
|
||||
// Submitted by Edward Hsing <contact@digitalplat.org>
|
||||
us.kg
|
||||
|
||||
// dnstrace.pro : https://dnstrace.pro/
|
||||
// Submitted by Chris Partridge <chris@partridge.tech>
|
||||
bci.dnstrace.pro
|
||||
|
@ -12959,6 +13046,14 @@ easypanel.host
|
|||
// Submitted by <infracloudteam@namecheap.com>
|
||||
*.ewp.live
|
||||
|
||||
// Electromagnetic Field : https://www.emfcamp.org
|
||||
// Submitted by <noc@emfcamp.org>
|
||||
at.emf.camp
|
||||
|
||||
// Elefunc, Inc. : https://elefunc.com
|
||||
// Submitted by Cetin Sert <domains@elefunc.com>
|
||||
rt.ht
|
||||
|
||||
// Elementor : Elementor Ltd.
|
||||
// Submitted by Anton Barkan <antonb@elementor.com>
|
||||
elementor.cloud
|
||||
|
@ -13061,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
|
||||
|
@ -13250,7 +13350,8 @@ forgeblocks.com
|
|||
id.forgerock.io
|
||||
|
||||
// Framer : https://www.framer.com
|
||||
// Submitted by Koen Rouwhorst <koenrh@framer.com>
|
||||
// Submitted by Koen Rouwhorst <security@framer.com>
|
||||
framer.ai
|
||||
framer.app
|
||||
framercanvas.com
|
||||
framer.media
|
||||
|
@ -13291,6 +13392,24 @@ freemyip.com
|
|||
// Submitted by Daniel A. Maierhofer <vorstand@funkfeuer.at>
|
||||
wien.funkfeuer.at
|
||||
|
||||
// Future Versatile Group. :https://www.fvg-on.net/
|
||||
// T.Kabu <webmaster@fvg-on.net>
|
||||
daemon.asia
|
||||
dix.asia
|
||||
mydns.bz
|
||||
0am.jp
|
||||
0g0.jp
|
||||
0j0.jp
|
||||
0t0.jp
|
||||
mydns.jp
|
||||
pgw.jp
|
||||
wjg.jp
|
||||
keyword-on.net
|
||||
live-on.net
|
||||
server-on.net
|
||||
mydns.tw
|
||||
mydns.vc
|
||||
|
||||
// Futureweb GmbH : https://www.futureweb.at
|
||||
// Submitted by Andreas Schnederle-Wagner <schnederle@futureweb.at>
|
||||
*.futurecms.at
|
||||
|
@ -13334,9 +13453,11 @@ gentlentapis.com
|
|||
lab.ms
|
||||
cdn-edges.net
|
||||
|
||||
// Ghost Foundation : https://ghost.org
|
||||
// Submitted by Matt Hanley <security@ghost.org>
|
||||
ghost.io
|
||||
// Getlocalcert: https://www.getlocalcert.net
|
||||
// Submitted by Robert Alexander <support@getlocalcert.net>
|
||||
localcert.net
|
||||
localhostcert.net
|
||||
corpnet.work
|
||||
|
||||
// GignoSystemJapan: http://gsj.bz
|
||||
// Submitted by GignoSystemJapan <kakutou-ec@gsj.bz>
|
||||
|
@ -13480,6 +13601,10 @@ whitesnow.jp
|
|||
zombie.jp
|
||||
heteml.net
|
||||
|
||||
// GoDaddy Registry : https://registry.godaddy
|
||||
// Submitted by Rohan Durrant <tldns@registry.godaddy>
|
||||
graphic.design
|
||||
|
||||
// GOV.UK Platform as a Service : https://www.cloud.service.gov.uk/
|
||||
// Submitted by Tom Whitwell <gov-uk-paas-support@digital.cabinet-office.gov.uk>
|
||||
cloudapps.digital
|
||||
|
@ -13498,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
|
||||
|
@ -13599,6 +13725,10 @@ goupile.fr
|
|||
// Submitted by <domeinnaam@minaz.nl>
|
||||
gov.nl
|
||||
|
||||
// GrayJay Web Solutions Inc. : https://grayjaysports.ca
|
||||
// Submitted by Matt Yamkowy <info@grayjaysports.ca>
|
||||
grayjayleagues.com
|
||||
|
||||
// Group 53, LLC : https://www.group53.com
|
||||
// Submitted by Tyler Todd <noc@nova53.net>
|
||||
awsmppl.com
|
||||
|
@ -13633,6 +13763,11 @@ hasura-app.io
|
|||
// Submitted by Richard Zowalla <mi-admin@hs-heilbronn.de>
|
||||
pages.it.hs-heilbronn.de
|
||||
|
||||
// Helio Networks : https://heliohost.org
|
||||
// Submitted by Ben Frede <admin@heliohost.org>
|
||||
helioho.st
|
||||
heliohost.us
|
||||
|
||||
// Hepforge : https://www.hepforge.org
|
||||
// Submitted by David Grellscheid <admin@hepforge.org>
|
||||
hepforge.org
|
||||
|
@ -13646,7 +13781,6 @@ herokussl.com
|
|||
// Submitted by Oren Eini <oren@ravendb.net>
|
||||
ravendb.cloud
|
||||
ravendb.community
|
||||
ravendb.me
|
||||
development.run
|
||||
ravendb.run
|
||||
|
||||
|
@ -13737,7 +13871,7 @@ biz.at
|
|||
info.at
|
||||
|
||||
// info.cx : http://info.cx
|
||||
// Submitted by Jacob Slater <whois@igloo.to>
|
||||
// Submitted by June Slater <whois@igloo.to>
|
||||
info.cx
|
||||
|
||||
// Interlegis : http://www.interlegis.leg.br
|
||||
|
@ -13786,6 +13920,14 @@ iopsys.se
|
|||
// Submitted by Matthew Hardeman <mhardeman@ipifony.com>
|
||||
ipifony.net
|
||||
|
||||
// is-a.dev : https://www.is-a.dev
|
||||
// Submitted by William Harrison <admin@maintainers.is-a.dev>
|
||||
is-a.dev
|
||||
|
||||
// ir.md : https://nic.ir.md
|
||||
// Submitted by Ali Soizi <info@nic.ir.md>
|
||||
ir.md
|
||||
|
||||
// IServ GmbH : https://iserv.de
|
||||
// Submitted by Mario Hoberg <info@iserv.de>
|
||||
iservschule.de
|
||||
|
@ -13894,6 +14036,11 @@ myjino.ru
|
|||
// Submitted by Daniel Fariña <ingenieria@jotelulu.com>
|
||||
jotelulu.cloud
|
||||
|
||||
// JouwWeb B.V. : https://www.jouwweb.nl
|
||||
// Submitted by Camilo Sperberg <tech@webador.com>
|
||||
jouwweb.site
|
||||
webadorsite.com
|
||||
|
||||
// Joyent : https://www.joyent.com/
|
||||
// Submitted by Brian Bennett <brian.bennett@joyent.com>
|
||||
*.triton.zone
|
||||
|
@ -13967,6 +14114,10 @@ lpusercontent.com
|
|||
// Submitted by Lelux Admin <publisuffix@lelux.site>
|
||||
lelux.site
|
||||
|
||||
// Libre IT Ltd : https://libre.nz
|
||||
// Submitted by Tomas Maggio <support@libre.nz>
|
||||
runcontainers.dev
|
||||
|
||||
// Lifetime Hosting : https://Lifetime.Hosting/
|
||||
// Submitted by Mike Fillator <support@lifetime.hosting>
|
||||
co.business
|
||||
|
@ -13977,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
|
||||
|
@ -14141,7 +14288,6 @@ co.pl
|
|||
// Managed by Corporate Domains
|
||||
// Microsoft Azure : https://home.azure
|
||||
*.azurecontainer.io
|
||||
*.cloudapp.azure.com
|
||||
azure-api.net
|
||||
azureedge.net
|
||||
azurefd.net
|
||||
|
@ -14227,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
|
||||
|
@ -14248,13 +14398,18 @@ ngrok.pro
|
|||
torun.pl
|
||||
|
||||
// Nimbus Hosting Ltd. : https://www.nimbushosting.co.uk/
|
||||
// Submitted by Nicholas Ford <nick@nimbushosting.co.uk>
|
||||
// Submitted by Nicholas Ford <dev@nimbushosting.co.uk>
|
||||
nh-serv.co.uk
|
||||
nimsite.uk
|
||||
|
||||
// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/
|
||||
// Submitted by Jeff Wheelhouse <support@nearlyfreespeech.net>
|
||||
nfshost.com
|
||||
|
||||
// NFT.Storage : https://nft.storage/
|
||||
// Submitted by Vasco Santos <vasco.santos@protocol.ai> or <support@nft.storage>
|
||||
ipfs.nftstorage.link
|
||||
|
||||
// Noop : https://noop.app
|
||||
// Submitted by Nathaniel Schweinberg <noop@rearc.io>
|
||||
*.developer.app
|
||||
|
@ -14272,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
|
||||
|
@ -14405,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/
|
||||
|
@ -14434,7 +14598,6 @@ omniwe.site
|
|||
123minsida.se
|
||||
123miweb.es
|
||||
123paginaweb.pt
|
||||
123sait.ru
|
||||
123siteweb.fr
|
||||
123webseite.at
|
||||
123webseite.de
|
||||
|
@ -14452,6 +14615,13 @@ simplesite.pl
|
|||
// Submitted by Eddie Jones <eddie@onefoldmedia.com>
|
||||
nid.io
|
||||
|
||||
// Open Domains : https://open-domains.net
|
||||
// Submitted by William Harrison <admin@open-domains.net>
|
||||
is-cool.dev
|
||||
is-not-a.dev
|
||||
localplayer.dev
|
||||
is-local.org
|
||||
|
||||
// Open Social : https://www.getopensocial.com/
|
||||
// Submitted by Alexander Varwijk <security@getopensocial.com>
|
||||
opensocial.site
|
||||
|
@ -14472,6 +14642,11 @@ operaunite.com
|
|||
// Submitted by Alexandre Linte <alexandre.linte@orange.com>
|
||||
tech.orange
|
||||
|
||||
// OsSav Technology Ltd. : https://ossav.com/
|
||||
// TLD Nic: http://nic.can.re - TLD Whois Server: whois.can.re
|
||||
// Submitted by OsSav Technology Ltd. <support@ossav.com>
|
||||
can.re
|
||||
|
||||
// Oursky Limited : https://authgear.com/, https://skygear.io/
|
||||
// Submitted by Authgear Team <hello@authgear.com>, Skygear Developer <hello@skygear.io>
|
||||
authgear-staging.com
|
||||
|
@ -14522,10 +14697,11 @@ pagexl.com
|
|||
|
||||
// pcarrier.ca Software Inc: https://pcarrier.ca/
|
||||
// Submitted by Pierre Carrier <pc@rrier.ca>
|
||||
bar0.net
|
||||
bar1.net
|
||||
bar2.net
|
||||
rdv.to
|
||||
*.xmit.co
|
||||
xmit.dev
|
||||
srv.us
|
||||
gh.srv.us
|
||||
gl.srv.us
|
||||
|
||||
// .pl domains (grandfathered)
|
||||
art.pl
|
||||
|
@ -14613,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
|
||||
|
@ -14683,9 +14855,12 @@ qcx.io
|
|||
*.sys.qcx.io
|
||||
|
||||
// QNAP System Inc : https://www.qnap.com
|
||||
// Submitted by Nick Chang <nickchang@qnap.com>
|
||||
dev-myqnapcloud.com
|
||||
// Submitted by Nick Chang <cloudadmin@qnap.com>
|
||||
myqnapcloud.cn
|
||||
alpha-myqnapcloud.com
|
||||
dev-myqnapcloud.com
|
||||
mycloudnas.com
|
||||
mynascloud.com
|
||||
myqnapcloud.com
|
||||
|
||||
// Quip : https://quip.com
|
||||
|
@ -14915,6 +15090,10 @@ service.gov.scot
|
|||
// Submitted by Shante Adam <shante@skyhat.io>
|
||||
scrysec.com
|
||||
|
||||
// Scrypted : https://scrypted.app
|
||||
// Submitted by Koushik Dutta <public-suffix-list@scrypted.app>
|
||||
client.scrypted.io
|
||||
|
||||
// Securepoint GmbH : https://www.securepoint.de
|
||||
// Submitted by Erik Anders <erik.anders@securepoint.de>
|
||||
firewall-gateway.com
|
||||
|
@ -14954,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
|
||||
|
@ -15024,9 +15207,9 @@ small-web.org
|
|||
vp4.me
|
||||
|
||||
// Snowflake Inc : https://www.snowflake.com/
|
||||
// Submitted by Faith Olapade <faith.olapade@snowflake.com>
|
||||
snowflake.app
|
||||
privatelink.snowflake.app
|
||||
// Submitted by Sam Haar <psl@snowflake.com>
|
||||
*.snowflake.app
|
||||
*.privatelink.snowflake.app
|
||||
streamlit.app
|
||||
streamlitapp.com
|
||||
|
||||
|
@ -15038,6 +15221,12 @@ try-snowplow.com
|
|||
// Submitted by Drew DeVault <sir@cmpwn.com>
|
||||
srht.site
|
||||
|
||||
// StackBlitz : https://stackblitz.com
|
||||
// Submitted by Dominic Elm <hello@stackblitz.com>
|
||||
w-corp-staticblitz.com
|
||||
w-credentialless-staticblitz.com
|
||||
w-staticblitz.com
|
||||
|
||||
// Stackhero : https://www.stackhero.io
|
||||
// Submitted by Adrien Gillon <adrien+public-suffix-list@stackhero.io>
|
||||
stackhero-network.com
|
||||
|
@ -15339,6 +15528,10 @@ inc.hk
|
|||
// Submitted by ITComdomains <to@it.com>
|
||||
it.com
|
||||
|
||||
// Unison Computing, PBC : https://unison.cloud
|
||||
// Submitted by Simon Højberg <security@unison.cloud>
|
||||
unison-services.cloud
|
||||
|
||||
// UNIVERSAL DOMAIN REGISTRY : https://www.udr.org.yt/
|
||||
// see also: whois -h whois.udr.org.yt help
|
||||
// Submitted by Atanunu Igbunuroghene <publicsuffixlist@udr.org.yt>
|
||||
|
@ -15366,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
|
||||
|
@ -15388,47 +15586,6 @@ v-info.info
|
|||
// Submitted by Nathan van Bakel <info@voorloper.com>
|
||||
voorloper.cloud
|
||||
|
||||
// Voxel.sh DNS : https://voxel.sh/dns/
|
||||
// Submitted by Mia Rehlinger <dns@voxel.sh>
|
||||
neko.am
|
||||
nyaa.am
|
||||
be.ax
|
||||
cat.ax
|
||||
es.ax
|
||||
eu.ax
|
||||
gg.ax
|
||||
mc.ax
|
||||
us.ax
|
||||
xy.ax
|
||||
nl.ci
|
||||
xx.gl
|
||||
app.gp
|
||||
blog.gt
|
||||
de.gt
|
||||
to.gt
|
||||
be.gy
|
||||
cc.hn
|
||||
io.kg
|
||||
jp.kg
|
||||
tv.kg
|
||||
uk.kg
|
||||
us.kg
|
||||
de.ls
|
||||
at.md
|
||||
de.md
|
||||
jp.md
|
||||
to.md
|
||||
indie.porn
|
||||
vxl.sh
|
||||
ch.tc
|
||||
me.tc
|
||||
we.tc
|
||||
nyan.to
|
||||
at.vg
|
||||
blog.vu
|
||||
dev.vu
|
||||
me.vu
|
||||
|
||||
// V.UA Domain Administrator : https://domain.v.ua/
|
||||
// Submitted by Serhii Rostilo <sergey@rostilo.kiev.ua>
|
||||
v.ua
|
||||
|
@ -15457,6 +15614,10 @@ reserve-online.com
|
|||
bookonline.app
|
||||
hotelwithflight.com
|
||||
|
||||
// WebWaddle Ltd: https://webwaddle.com/
|
||||
// Submitted by Merlin Glander <hostmaster@webwaddle.com>
|
||||
*.wadl.top
|
||||
|
||||
// WeDeploy by Liferay, Inc. : https://www.wedeploy.com
|
||||
// Submitted by Henrique Vicente <security@wedeploy.com>
|
||||
wedeploy.io
|
||||
|
@ -15467,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
|
||||
|
@ -15569,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);
|
||||
|
||||
|
|
|
@ -155,6 +155,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
private AdapterNavMenu adapterNavMenu;
|
||||
private AdapterNavMenu adapterNavMenuExtra;
|
||||
|
||||
private boolean initialized = false;
|
||||
private boolean exit = false;
|
||||
private boolean searching = false;
|
||||
private int lastBackStackCount = 0;
|
||||
|
@ -217,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);
|
||||
|
||||
|
@ -266,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);
|
||||
|
@ -744,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();
|
||||
|
||||
|
@ -779,6 +783,42 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
private void init() {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
if ("inbox".equals(startup)) {
|
||||
new SimpleTask<EntityFolder>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EntityFolder onExecute(Context context, Bundle args) throws Throwable {
|
||||
DB db = DB.getInstance(context);
|
||||
return db.folder().getFolderPrimary(EntityFolder.INBOX);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExecuted(Bundle args, EntityFolder inbox) {
|
||||
FragmentBase fragment = new FragmentMessages();
|
||||
if (inbox != null) {
|
||||
args.putString("type", inbox.type);
|
||||
args.putLong("account", inbox.account);
|
||||
args.putLong("folder", inbox.id);
|
||||
}
|
||||
fragment.setArguments(args);
|
||||
setFragment(fragment);
|
||||
checkIntent();
|
||||
checkFirst();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getSupportFragmentManager(), ex);
|
||||
}
|
||||
}.execute(this, new Bundle(), "primary");
|
||||
return;
|
||||
}
|
||||
|
||||
FragmentBase fragment;
|
||||
switch (startup) {
|
||||
case "accounts":
|
||||
|
@ -798,7 +838,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
}
|
||||
|
||||
fragment.setArguments(args);
|
||||
setFragment(fragment);
|
||||
}
|
||||
|
||||
private void setFragment(Fragment fragment) {
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fm.beginTransaction();
|
||||
for (Fragment existing : fm.getFragments())
|
||||
|
@ -811,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);
|
||||
}
|
||||
|
@ -1127,7 +1171,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
|
||||
checkUpdate(false);
|
||||
checkAnnouncements(false);
|
||||
checkIntent();
|
||||
if (initialized || !"inbox".equals(startup))
|
||||
checkIntent();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1346,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);
|
||||
|
@ -1485,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();
|
||||
|
@ -1492,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);
|
||||
|
@ -1288,6 +1293,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
message.folderUnified && outgoing) ||
|
||||
EntityFolder.isOutgoing(message.folderInheritedType));
|
||||
String selector = (reverse ? null : message.bimi_selector);
|
||||
boolean dmarc = (!reverse && Boolean.TRUE.equals(message.dmarc));
|
||||
Address[] addresses = (reverse ? message.to : (message.isForwarder() ? message.submitter : message.from));
|
||||
Address[] senders = ContactInfo.fillIn(
|
||||
reverse && !show_recipients ? message.to : message.senders, prefer_contact, only_contact);
|
||||
|
@ -1661,7 +1667,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
// Contact info
|
||||
ContactInfo[] info = ContactInfo.getCached(context,
|
||||
message.account, message.folderType, selector, addresses);
|
||||
message.account, message.folderType, selector, dmarc, addresses);
|
||||
if (info == null) {
|
||||
if (taskContactInfo != null) {
|
||||
taskContactInfo.cancel(context);
|
||||
|
@ -1673,6 +1679,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
aargs.putLong("account", message.account);
|
||||
aargs.putString("folderType", message.folderType);
|
||||
aargs.putString("selector", selector);
|
||||
aargs.putBoolean("dmarc", dmarc);
|
||||
aargs.putSerializable("addresses", addresses);
|
||||
|
||||
taskContactInfo = new SimpleTask<ContactInfo[]>() {
|
||||
|
@ -1681,8 +1688,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
long account = args.getLong("account");
|
||||
String folderType = args.getString("folderType");
|
||||
String selector = args.getString("selector");
|
||||
boolean dmarc = args.getBoolean("dmarc");
|
||||
Address[] addresses = (Address[]) args.getSerializable("addresses");
|
||||
return ContactInfo.get(context, account, folderType, selector, addresses);
|
||||
return ContactInfo.get(context, account, folderType, selector, dmarc, addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1839,6 +1847,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 +2144,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);
|
||||
|
@ -2335,6 +2345,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean expand_all = prefs.getBoolean("expand_all", false);
|
||||
boolean expand_one = prefs.getBoolean("expand_one", true);
|
||||
boolean swipe_reply = prefs.getBoolean("swipe_reply", false);
|
||||
boolean tools = prefs.getBoolean("message_tools", true);
|
||||
boolean button_junk = prefs.getBoolean("button_junk", true);
|
||||
boolean button_trash = prefs.getBoolean("button_trash", true);
|
||||
|
@ -2348,6 +2359,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);
|
||||
|
@ -2361,7 +2373,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
boolean button_raw = prefs.getBoolean("button_raw", false);
|
||||
boolean button_unsubscribe = prefs.getBoolean("button_unsubscribe", true);
|
||||
boolean button_rule = prefs.getBoolean("button_rule", false);
|
||||
boolean swipe_reply = prefs.getBoolean("swipe_reply", false);
|
||||
boolean button_answer = prefs.getBoolean("button_answer", false);
|
||||
|
||||
int importance = (((message.ui_importance == null ? 1 : message.ui_importance) + 1) % 3);
|
||||
|
||||
|
@ -2375,7 +2387,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibInbox.setImageResource(inJunk ? R.drawable.twotone_report_off_24 : R.drawable.twotone_inbox_24);
|
||||
|
||||
ibUndo.setVisibility(outbox ? View.VISIBLE : View.GONE);
|
||||
ibAnswer.setVisibility(!tools || outbox || (!expand_all && expand_one) || !threading || swipe_reply ? View.GONE : View.VISIBLE);
|
||||
ibAnswer.setVisibility(!tools || outbox || (!expand_all && expand_one && !button_answer) || (!threading && !button_answer) || (swipe_reply && !button_answer) ? View.GONE : View.VISIBLE);
|
||||
ibRule.setVisibility(tools && button_rule && !outbox && !message.folderReadOnly ? View.VISIBLE : View.GONE);
|
||||
ibUnsubscribe.setVisibility(tools && button_unsubscribe && message.unsubscribe != null ? View.VISIBLE : View.GONE);
|
||||
ibRaw.setVisibility(tools && button_raw && raw ? View.VISIBLE : View.GONE);
|
||||
|
@ -2388,6 +2400,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);
|
||||
|
@ -2545,7 +2558,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) {
|
||||
|
@ -2553,6 +2566,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)) {
|
||||
|
@ -2840,8 +2860,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
properties.setValue("images_asked", message.id, true);
|
||||
}
|
||||
|
||||
if (message.from != null)
|
||||
for (Address sender : message.from) {
|
||||
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
|
||||
|
||||
if (senders != null)
|
||||
for (Address sender : senders) {
|
||||
String from = ((InternetAddress) sender).getAddress();
|
||||
if (TextUtils.isEmpty(from))
|
||||
continue;
|
||||
|
@ -4437,7 +4459,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)
|
||||
|
@ -4526,6 +4548,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) {
|
||||
|
@ -4889,12 +4913,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");
|
||||
|
@ -4908,61 +4935,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) {
|
||||
|
@ -4972,6 +4986,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();
|
||||
}
|
||||
|
||||
|
@ -5433,7 +5450,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;
|
||||
}
|
||||
|
||||
|
@ -5524,15 +5546,16 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
|
||||
boolean junk = EntityFolder.JUNK.equals(message.folderType);
|
||||
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
|
||||
|
||||
boolean current = properties.getValue(full ? "full" : "images", message.id);
|
||||
boolean asked = properties.getValue(full ? "full_asked" : "images_asked", message.id);
|
||||
boolean confirm = prefs.getBoolean(full ? "confirm_html" : "confirm_images", true) || junk;
|
||||
boolean ask = prefs.getBoolean(full ? "ask_html" : "ask_images", true) || junk;
|
||||
if (current || asked || !confirm || !ask) {
|
||||
if (current && message.from != null) {
|
||||
if (current && senders != null) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
for (Address sender : message.from) {
|
||||
for (Address sender : senders) {
|
||||
String from = ((InternetAddress) sender).getAddress();
|
||||
if (TextUtils.isEmpty(from))
|
||||
continue;
|
||||
|
@ -5562,13 +5585,13 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
cbNotAgainSender.setVisibility(View.GONE);
|
||||
cbNotAgainDomain.setVisibility(View.GONE);
|
||||
cbNotAgain.setVisibility(View.GONE);
|
||||
} else if (message.from == null || message.from.length == 0) {
|
||||
} else if (senders == null || senders.length == 0) {
|
||||
cbNotAgainSender.setVisibility(View.GONE);
|
||||
cbNotAgainDomain.setVisibility(View.GONE);
|
||||
} else {
|
||||
List<String> froms = new ArrayList<>();
|
||||
List<String> domains = new ArrayList<>();
|
||||
for (Address address : message.from) {
|
||||
for (Address address : senders) {
|
||||
String from = ((InternetAddress) address).getAddress();
|
||||
froms.add(from);
|
||||
int at = from.indexOf('@');
|
||||
|
@ -5640,8 +5663,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
if (!junk) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
if (message.from != null)
|
||||
for (Address sender : message.from) {
|
||||
if (senders != null)
|
||||
for (Address sender : senders) {
|
||||
String from = ((InternetAddress) sender).getAddress();
|
||||
if (TextUtils.isEmpty(from))
|
||||
continue;
|
||||
|
@ -6181,7 +6204,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)
|
||||
|
@ -6235,6 +6258,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);
|
||||
|
@ -6344,6 +6369,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;
|
||||
|
@ -6420,6 +6448,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
Log.i("Opening uri=" + uri + " title=" + title + " always confirm=" + always_confirm);
|
||||
|
||||
try {
|
||||
uri = Uri.parse(uri.toString().replaceAll("[\r\n]", ""));
|
||||
if (UriHelper.isHyperLink(uri))
|
||||
uri = Uri.parse(uri.toString().trim().replaceAll("\\s+", "+"));
|
||||
|
||||
|
@ -6456,8 +6485,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
try {
|
||||
if (ActivityBilling.activatePro(context, uri))
|
||||
ToastEx.makeText(context, R.string.title_pro_valid, Toast.LENGTH_LONG).show();
|
||||
else
|
||||
ToastEx.makeText(context, R.string.title_pro_invalid, Toast.LENGTH_LONG).show();
|
||||
else {
|
||||
Uri invalid = Uri.parse(BuildConfig.PRO_FEATURES_URI + "invalid.html" +
|
||||
"?challenge=" + ActivityBilling.getChallenge(context) +
|
||||
"&version=" + BuildConfig.VERSION_CODE);
|
||||
Helper.view(context, invalid, true);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.e(ex);
|
||||
ToastEx.makeText(context, Log.formatThrowable(ex), Toast.LENGTH_LONG).show();
|
||||
|
@ -6569,6 +6602,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
|
||||
private boolean isActivate(Uri uri) {
|
||||
if ("eu.faircode.email".equals(uri.getHost()))
|
||||
return true;
|
||||
return ("email.faircode.eu".equals(uri.getHost()) &&
|
||||
"/activate/".equals(uri.getPath()));
|
||||
}
|
||||
|
@ -7245,6 +7280,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);
|
||||
|
@ -7466,6 +7505,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
args.putLong("account", message.account);
|
||||
args.putString("folderType", message.folderType);
|
||||
args.putString("selector", message.bimi_selector);
|
||||
args.putBoolean("dmarc", Boolean.TRUE.equals(message.dmarc));
|
||||
args.putSerializable("addresses", message.from);
|
||||
|
||||
new SimpleTask<ContactInfo[]>() {
|
||||
|
@ -7474,8 +7514,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
long account = args.getLong("account");
|
||||
String folderType = args.getString("folderType");
|
||||
String selector = args.getString("selector");
|
||||
boolean dmarc = args.getBoolean("dmarc");
|
||||
Address[] addresses = (Address[]) args.getSerializable("addresses");
|
||||
return ContactInfo.get(context, account, folderType, selector, addresses);
|
||||
return ContactInfo.get(context, account, folderType, selector, dmarc, addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7750,7 +7791,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);
|
||||
|
@ -7787,63 +7828,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
|
||||
|
@ -7877,11 +7937,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;
|
||||
|
@ -8470,6 +8533,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)) {
|
||||
|
@ -8709,12 +8773,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;
|
||||
|
@ -8742,6 +8806,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,14 +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),
|
||||
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();
|
||||
|
@ -605,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);
|
||||
}
|
||||
|
@ -612,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;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,12 @@ package eu.faircode.email;
|
|||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
|
@ -40,6 +42,7 @@ import java.net.URL;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -403,6 +406,9 @@ public class Adguard {
|
|||
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
|
||||
Helper.copy(connection.getInputStream(), os);
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putLong("adguard_last", new Date().getTime()).apply();
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
|
|
|
@ -406,19 +406,14 @@ public class ApplicationEx extends Application
|
|||
case "watchdog":
|
||||
ServiceSynchronize.scheduleWatchdog(this);
|
||||
break;
|
||||
case "secure": // privacy
|
||||
case "load_emoji": // privacy
|
||||
case "shortcuts": // misc
|
||||
case "language": // misc
|
||||
case "wal": // misc
|
||||
// Should be excluded for import
|
||||
restart(this, key);
|
||||
break;
|
||||
case "debug":
|
||||
case "log_level":
|
||||
Log.setLevel(this);
|
||||
FairEmailLoggingProvider.setLevel(this);
|
||||
break;
|
||||
default:
|
||||
if (FragmentOptionsBackup.RESTART_OPTIONS.contains(key))
|
||||
restart(this, key);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
|
@ -857,6 +852,13 @@ public class ApplicationEx extends Application
|
|||
} else if (version < 2168) {
|
||||
if (Helper.isGoogle())
|
||||
editor.putBoolean("mod", true);
|
||||
} else if (version < 2170) {
|
||||
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)
|
||||
|
@ -866,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();
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
private static ExecutorService executor = Helper.getBackgroundExecutor(1, "boundary");
|
||||
|
||||
private static final int SEARCH_LIMIT_DEVICE = 1000;
|
||||
private static final int FETCH_LIMIT_SERVER = 100000;
|
||||
|
||||
interface IBoundaryCallbackMessages {
|
||||
void onLoading();
|
||||
|
@ -357,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 +
|
||||
|
@ -470,7 +472,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
|
||||
|
||||
if (and.size() == 0)
|
||||
state.imessages = state.ifolder.getMessages();
|
||||
state.getMessages(FETCH_LIMIT_SERVER);
|
||||
else
|
||||
state.imessages = state.ifolder.search(new AndTerm(and.toArray(new SearchTerm[0])));
|
||||
|
||||
|
@ -723,8 +725,10 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
EntityLog.log(context, "Search utf8=" + utf8);
|
||||
|
||||
SearchTerm terms = criteria.getTerms(utf8, state.ifolder.getPermanentFlags(), keywords);
|
||||
if (terms == null)
|
||||
return state.ifolder.getMessages();
|
||||
if (terms == null) {
|
||||
state.getMessages(FETCH_LIMIT_SERVER);
|
||||
return state.imessages;
|
||||
}
|
||||
|
||||
SearchSequence ss = new SearchSequence(protocol);
|
||||
Argument args = ss.generateSequence(terms, utf8 ? StandardCharsets.UTF_8.name() : null);
|
||||
|
@ -802,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;
|
||||
}
|
||||
|
||||
|
@ -876,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;
|
||||
}
|
||||
|
@ -991,6 +1000,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
IMAPFolder ifolder = null;
|
||||
Message[] imessages = null;
|
||||
|
||||
void getMessages(int max) throws MessagingException {
|
||||
int total = Math.min(ifolder.getMessageCount(), max);
|
||||
imessages = new Message[total];
|
||||
for (int i = 1; i <= total; i++)
|
||||
imessages[i - 1] = ifolder.getMessage(i);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
Log.i("Boundary reset");
|
||||
queued.set(0);
|
||||
|
@ -1032,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:";
|
||||
|
@ -1285,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);
|
||||
|
@ -1316,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;
|
||||
}
|
||||
|
@ -1366,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");
|
||||
|
@ -1414,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;
|
||||
}
|
||||
|
||||
|
@ -1442,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
|
||||
|
|
|
@ -219,15 +219,15 @@ public class ContactInfo {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
static ContactInfo[] get(Context context, long account, String folderType, String selector, Address[] addresses) {
|
||||
return get(context, account, folderType, selector, addresses, false);
|
||||
static ContactInfo[] get(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses) {
|
||||
return get(context, account, folderType, selector, dmarc, addresses, false);
|
||||
}
|
||||
|
||||
static ContactInfo[] getCached(Context context, long account, String folderType, String selector, Address[] addresses) {
|
||||
return get(context, account, folderType, selector, addresses, true);
|
||||
static ContactInfo[] getCached(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses) {
|
||||
return get(context, account, folderType, selector, dmarc, addresses, true);
|
||||
}
|
||||
|
||||
private static ContactInfo[] get(Context context, long account, String folderType, String selector, Address[] addresses, boolean cacheOnly) {
|
||||
private static ContactInfo[] get(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses, boolean cacheOnly) {
|
||||
if (addresses == null || addresses.length == 0) {
|
||||
ContactInfo anonymous = getAnonymous(context);
|
||||
return new ContactInfo[]{anonymous == null ? new ContactInfo() : anonymous};
|
||||
|
@ -235,7 +235,7 @@ public class ContactInfo {
|
|||
|
||||
ContactInfo[] result = new ContactInfo[addresses.length];
|
||||
for (int i = 0; i < addresses.length; i++) {
|
||||
result[i] = _get(context, account, folderType, selector, (InternetAddress) addresses[i], cacheOnly);
|
||||
result[i] = _get(context, account, folderType, selector, dmarc, (InternetAddress) addresses[i], cacheOnly);
|
||||
if (result[i] == null) {
|
||||
if (cacheOnly)
|
||||
return null;
|
||||
|
@ -257,7 +257,7 @@ public class ContactInfo {
|
|||
private static ContactInfo _get(
|
||||
Context context,
|
||||
long account, String folderType,
|
||||
String selector, InternetAddress address, boolean cacheOnly) {
|
||||
String selector, boolean dmarc, InternetAddress address, boolean cacheOnly) {
|
||||
String key = MessageHelper.formatAddresses(new Address[]{address});
|
||||
synchronized (emailContactInfo) {
|
||||
ContactInfo info = emailContactInfo.get(key);
|
||||
|
@ -277,7 +277,7 @@ public class ContactInfo {
|
|||
boolean avatars = prefs.getBoolean("avatars", true);
|
||||
boolean prefer_contact = prefs.getBoolean("prefer_contact", false);
|
||||
boolean distinguish_contacts = prefs.getBoolean("distinguish_contacts", false);
|
||||
boolean bimi = (prefs.getBoolean("bimi", false) && !BuildConfig.PLAY_STORE_RELEASE);
|
||||
boolean bimi = (prefs.getBoolean("bimi", false) && dmarc && !BuildConfig.PLAY_STORE_RELEASE);
|
||||
boolean gravatars = (prefs.getBoolean("gravatars", false) && !BuildConfig.PLAY_STORE_RELEASE);
|
||||
boolean libravatars = (prefs.getBoolean("libravatars", false) && !BuildConfig.PLAY_STORE_RELEASE);
|
||||
boolean favicons = prefs.getBoolean("favicons", false);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -3514,6 +3518,8 @@ class Core {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!Boolean.TRUE.equals(message.dkim))
|
||||
message.dmarc = message.dkim;
|
||||
}
|
||||
|
||||
if (message.size == null && message.total != null)
|
||||
|
@ -3609,7 +3615,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,
|
||||
|
@ -4676,6 +4682,8 @@ class Core {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!Boolean.TRUE.equals(message.dkim))
|
||||
message.dmarc = message.dkim;
|
||||
}
|
||||
|
||||
// Borrow reply name from sender name
|
||||
|
@ -4896,7 +4904,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);
|
||||
|
@ -5204,14 +5212,16 @@ class Core {
|
|||
if (message.from != null)
|
||||
addresses.addAll(Arrays.asList(message.from));
|
||||
} else {
|
||||
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
|
||||
|
||||
if (message.to != null)
|
||||
addresses.addAll(Arrays.asList(message.to));
|
||||
if (message.cc != null)
|
||||
addresses.addAll(Arrays.asList(message.cc));
|
||||
if (message.bcc != null)
|
||||
addresses.addAll(Arrays.asList(message.bcc));
|
||||
if (message.from != null)
|
||||
addresses.addAll(Arrays.asList(message.from));
|
||||
if (senders != null)
|
||||
addresses.addAll(Arrays.asList(senders));
|
||||
}
|
||||
|
||||
InternetAddress deliveredto = null;
|
||||
|
@ -5414,7 +5424,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) {
|
||||
|
|
|
@ -245,8 +245,8 @@ public interface DaoFolder {
|
|||
" JOIN account ON account.id = folder.account" +
|
||||
" WHERE account.synchronize" +
|
||||
" AND account.`primary`" +
|
||||
" AND type = '" + EntityFolder.DRAFTS + "'")
|
||||
EntityFolder getPrimaryDrafts();
|
||||
" AND type = :type")
|
||||
EntityFolder getFolderPrimary(String type);
|
||||
|
||||
@Query("SELECT * FROM folder WHERE type = '" + EntityFolder.OUTBOX + "'")
|
||||
EntityFolder getOutbox();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -79,9 +79,7 @@ public class DeepL {
|
|||
|
||||
static final String PRIVACY_URI = "https://www.deepl.com/privacy/";
|
||||
|
||||
// curl https://api-free.deepl.com/v2/languages \
|
||||
// -d auth_key=... \
|
||||
// -d type=target
|
||||
// curl https://api-free.deepl.com/v2/languages -d auth_key=... -d type=target
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
@ -117,7 +115,9 @@ public class DeepL {
|
|||
int frequency = prefs.getInt("translated_" + target, 0);
|
||||
|
||||
String flag;
|
||||
if ("CS".equals(target))
|
||||
if ("AR".equals(target))
|
||||
flag = "SA";
|
||||
else if ("CS".equals(target))
|
||||
flag = "CZ";
|
||||
else if ("DA".equals(target))
|
||||
flag = "DK";
|
||||
|
|
|
@ -37,6 +37,7 @@ import java.io.IOException;
|
|||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -132,6 +133,9 @@ public class DisconnectBlacklist {
|
|||
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
|
||||
Helper.copy(connection.getInputStream(), os);
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putLong("disconnect_last", new Date().getTime()).apply();
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -182,7 +181,7 @@ public class DnsHelper {
|
|||
throw new IllegalArgumentException(type);
|
||||
}
|
||||
|
||||
ResolverApi resolver = DnssecResolverApi.INSTANCE;
|
||||
ResolverApi resolver = (dnssec ? DnssecResolverApi.INSTANCE : ResolverApi.INSTANCE);
|
||||
AbstractDnsClient client = resolver.getClient();
|
||||
|
||||
if (false) {
|
||||
|
@ -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 "???";
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue