mirror of https://github.com/M66B/FairEmail.git
Merge branch 'master' of github.com:M66B/FairEmail
This commit is contained in:
commit
9ef4b0a99a
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -6,6 +6,17 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
|
||||
### [Saltopus](https://en.wikipedia.org/wiki/Saltopus)
|
||||
|
||||
### Next version
|
||||
|
||||
* Added filter rule groups
|
||||
* Added executing of filter rules by automation apps, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71)
|
||||
* Added auto completion for reply template groups and filter rule groups
|
||||
* Improved auto-discovery via DNS SRV records
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2060 - 2023-04-01
|
||||
|
||||
* Added draft message printing
|
||||
|
|
13
FAQ.md
13
FAQ.md
|
@ -2768,6 +2768,15 @@ The filter rules will move the messages to a (sub) archive folder as a second st
|
|||
|
||||
The POP3 protocol does not support setting keywords and moving or copying messages.
|
||||
|
||||
<br />
|
||||
|
||||
Since version 1.2061 it is possible to execute rules with an automation app, like for example Tasker.
|
||||
|
||||
|
||||
```
|
||||
(adb shell) am start-foreground-service -a eu.faircode.email.RULE --es account <account name> -e rule <unique rule name>
|
||||
```
|
||||
|
||||
Using rules is a pro feature.
|
||||
|
||||
<br />
|
||||
|
@ -3862,7 +3871,7 @@ FairEmail fetches a message in two steps:
|
|||
|
||||
Directly after the first step new messages will be notified.
|
||||
However, only until after the second step the message text will be available.
|
||||
FairEmail updates exiting notifications with a preview of the message text, but unfortunately wearable notifications cannot be updated.
|
||||
FairEmail updates existing notifications with a preview of the message text, but unfortunately wearable notifications cannot be updated.
|
||||
|
||||
Since there is no guarantee that a message text will always be fetched directly after a message header,
|
||||
it is not possible to guarantee that a new message notification with a preview text will always be sent to a wearable.
|
||||
|
@ -4038,7 +4047,7 @@ Individual messages will rarely be trashed and mostly this happens by accident.
|
|||
Showing trashed messages in conversations makes it easier to find them back.
|
||||
|
||||
You can permanently delete a message using the message three-dots *delete* menu, which will remove the message from the conversation.
|
||||
Note that this irreversible.
|
||||
Note that this is irreversible.
|
||||
|
||||
Similarly, drafts are shown in conversations to find them back in the context where they belong.
|
||||
It is easy to read through the received messages before continuing to write the draft later.
|
||||
|
|
|
@ -14,7 +14,7 @@ FairEmail might be for you if you value your privacy.
|
|||
|
||||
<i>Almost all features are free to use, but to maintain and support the app in the long term, not every feature can be for free. See below for a list of pro features.</i>
|
||||
|
||||
<i>A lot of effort has gone into this app, which was developed to help you protect your privacy. If you have a question or problem, there is always support at marcel@faircode.eu.</i>
|
||||
<i>A lot of effort has gone into this mail app, which was developed to help you protect your privacy. If you have a question or problem, there is always support at marcel@faircode.eu.</i>
|
||||
|
||||
<b>Main features</b>
|
||||
|
||||
|
|
|
@ -439,18 +439,18 @@ dependencies {
|
|||
|
||||
def startup_version = "1.1.1" // 1.2.0-alpha02
|
||||
def annotation_version_experimental = "1.3.0"
|
||||
def core_version = "1.10.0-rc01" // 1.11.0-alpha01
|
||||
def core_version = "1.10.0"
|
||||
def appcompat_version = "1.6.1" // 1.7.0-alpha02
|
||||
def emoji_version = "1.3.0" // 1.4.0-alpha01
|
||||
def emoji_version = "1.3.0" // 1.4.0-beta01
|
||||
def flatbuffers_version = "2.0.0"
|
||||
def activity_version = "1.7.0"
|
||||
def fragment_version = "1.5.6" // 1.6.0-alpha08
|
||||
def windows_version = "1.0.0" // 1.1.0-alpha06
|
||||
def webkit_version = "1.6.1" // 1.7.0-alpha03
|
||||
def fragment_version = "1.5.6" // 1.6.0-alpha09
|
||||
def windows_version = "1.0.0" // 1.1.0-beta02
|
||||
def webkit_version = "1.6.1" // 1.7.0-beta01
|
||||
def recyclerview_version = "1.3.0"
|
||||
def coordinatorlayout_version = "1.2.0"
|
||||
def constraintlayout_version = "2.1.4" // 2.2.0-alpha09
|
||||
def material_version = "1.8.0"
|
||||
def material_version = "1.9.0-beta01" // 1.10.0-alpha01
|
||||
def browser_version = "1.5.0"
|
||||
def lbm_version = "1.1.0"
|
||||
def swiperefresh_version = "1.2.0-alpha01"
|
||||
|
|
|
@ -561,6 +561,7 @@
|
|||
<action android:name="${applicationId}.ENABLE" />
|
||||
<action android:name="${applicationId}.DISABLE" />
|
||||
<action android:name="${applicationId}.INTERVAL" />
|
||||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
@ -83,7 +83,7 @@ public class Avatar {
|
|||
for (String dns : LIBRAVATAR_DNS.split(",")) {
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns + "." + domain, "srv");
|
||||
if (records.length > 0) {
|
||||
baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].name + "/avatar/";
|
||||
baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].response + "/avatar/";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ public class Bimi {
|
|||
}
|
||||
|
||||
// Process DNS record
|
||||
Map<String, String> values = MessageHelper.getKeyValues(record.name);
|
||||
Map<String, String> values = MessageHelper.getKeyValues(record.response);
|
||||
List<String> tags = new ArrayList<>(values.keySet());
|
||||
Collections.sort(tags); // process certificate first
|
||||
for (String tag : tags) {
|
||||
|
@ -337,9 +337,9 @@ public class Bimi {
|
|||
}
|
||||
if (records.length == 0)
|
||||
throw new IllegalArgumentException("DMARC missing");
|
||||
Log.i("BIMI got TXT " + records[0].name);
|
||||
Log.i("BIMI got TXT " + records[0].response);
|
||||
|
||||
Map<String, String> dmarc = MessageHelper.getKeyValues(records[0].name);
|
||||
Map<String, String> dmarc = MessageHelper.getKeyValues(records[0].response);
|
||||
|
||||
String p = dmarc.get("p");
|
||||
if (p == null ||
|
||||
|
@ -376,7 +376,7 @@ public class Bimi {
|
|||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, txt, "txt");
|
||||
if (records.length == 0)
|
||||
return null;
|
||||
Log.i("BIMI got TXT " + records[0].name);
|
||||
Log.i("BIMI got TXT " + records[0].response);
|
||||
return records[0];
|
||||
} catch (Throwable ex) {
|
||||
Log.i(ex);
|
||||
|
|
|
@ -567,6 +567,7 @@
|
|||
<action android:name="${applicationId}.ENABLE" />
|
||||
<action android:name="${applicationId}.DISABLE" />
|
||||
<action android:name="${applicationId}.INTERVAL" />
|
||||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
@ -567,6 +567,7 @@
|
|||
<action android:name="${applicationId}.ENABLE" />
|
||||
<action android:name="${applicationId}.DISABLE" />
|
||||
<action android:name="${applicationId}.INTERVAL" />
|
||||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
@ -559,6 +559,7 @@
|
|||
<action android:name="${applicationId}.ENABLE" />
|
||||
<action android:name="${applicationId}.DISABLE" />
|
||||
<action android:name="${applicationId}.INTERVAL" />
|
||||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
@ -6,6 +6,17 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
|
|||
|
||||
### [Saltopus](https://en.wikipedia.org/wiki/Saltopus)
|
||||
|
||||
### Next version
|
||||
|
||||
* Added filter rule groups
|
||||
* Added executing of filter rules by automation apps, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71)
|
||||
* Added auto completion for reply template groups and filter rule groups
|
||||
* Improved auto-discovery via DNS SRV records
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
|
||||
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
|
||||
* Updated [translations](https://crowdin.com/project/open-source-email)
|
||||
|
||||
### 1.2060 - 2023-04-01
|
||||
|
||||
* Added draft message printing
|
||||
|
|
|
@ -1051,8 +1051,7 @@ fm
|
|||
// fo : https://en.wikipedia.org/wiki/.fo
|
||||
fo
|
||||
|
||||
// fr : http://www.afnic.fr/
|
||||
// domaines descriptifs : https://www.afnic.fr/medias/documents/Cadre_legal/Afnic_Naming_Policy_12122016_VEN.pdf
|
||||
// fr : https://www.afnic.fr/ https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
fr
|
||||
asso.fr
|
||||
com.fr
|
||||
|
@ -1060,7 +1059,7 @@ gouv.fr
|
|||
nom.fr
|
||||
prd.fr
|
||||
tm.fr
|
||||
// domaines sectoriels : https://www.afnic.fr/en/products-and-services/the-fr-tld/sector-based-fr-domains-4.html
|
||||
// Former "domaines sectoriels", still registration suffixes
|
||||
aeroport.fr
|
||||
avocat.fr
|
||||
avoues.fr
|
||||
|
@ -4070,555 +4069,8 @@ ac.mu
|
|||
co.mu
|
||||
or.mu
|
||||
|
||||
// museum : http://about.museum/naming/
|
||||
// http://index.museum/
|
||||
// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/
|
||||
museum
|
||||
academy.museum
|
||||
agriculture.museum
|
||||
air.museum
|
||||
airguard.museum
|
||||
alabama.museum
|
||||
alaska.museum
|
||||
amber.museum
|
||||
ambulance.museum
|
||||
american.museum
|
||||
americana.museum
|
||||
americanantiques.museum
|
||||
americanart.museum
|
||||
amsterdam.museum
|
||||
and.museum
|
||||
annefrank.museum
|
||||
anthro.museum
|
||||
anthropology.museum
|
||||
antiques.museum
|
||||
aquarium.museum
|
||||
arboretum.museum
|
||||
archaeological.museum
|
||||
archaeology.museum
|
||||
architecture.museum
|
||||
art.museum
|
||||
artanddesign.museum
|
||||
artcenter.museum
|
||||
artdeco.museum
|
||||
arteducation.museum
|
||||
artgallery.museum
|
||||
arts.museum
|
||||
artsandcrafts.museum
|
||||
asmatart.museum
|
||||
assassination.museum
|
||||
assisi.museum
|
||||
association.museum
|
||||
astronomy.museum
|
||||
atlanta.museum
|
||||
austin.museum
|
||||
australia.museum
|
||||
automotive.museum
|
||||
aviation.museum
|
||||
axis.museum
|
||||
badajoz.museum
|
||||
baghdad.museum
|
||||
bahn.museum
|
||||
bale.museum
|
||||
baltimore.museum
|
||||
barcelona.museum
|
||||
baseball.museum
|
||||
basel.museum
|
||||
baths.museum
|
||||
bauern.museum
|
||||
beauxarts.museum
|
||||
beeldengeluid.museum
|
||||
bellevue.museum
|
||||
bergbau.museum
|
||||
berkeley.museum
|
||||
berlin.museum
|
||||
bern.museum
|
||||
bible.museum
|
||||
bilbao.museum
|
||||
bill.museum
|
||||
birdart.museum
|
||||
birthplace.museum
|
||||
bonn.museum
|
||||
boston.museum
|
||||
botanical.museum
|
||||
botanicalgarden.museum
|
||||
botanicgarden.museum
|
||||
botany.museum
|
||||
brandywinevalley.museum
|
||||
brasil.museum
|
||||
bristol.museum
|
||||
british.museum
|
||||
britishcolumbia.museum
|
||||
broadcast.museum
|
||||
brunel.museum
|
||||
brussel.museum
|
||||
brussels.museum
|
||||
bruxelles.museum
|
||||
building.museum
|
||||
burghof.museum
|
||||
bus.museum
|
||||
bushey.museum
|
||||
cadaques.museum
|
||||
california.museum
|
||||
cambridge.museum
|
||||
can.museum
|
||||
canada.museum
|
||||
capebreton.museum
|
||||
carrier.museum
|
||||
cartoonart.museum
|
||||
casadelamoneda.museum
|
||||
castle.museum
|
||||
castres.museum
|
||||
celtic.museum
|
||||
center.museum
|
||||
chattanooga.museum
|
||||
cheltenham.museum
|
||||
chesapeakebay.museum
|
||||
chicago.museum
|
||||
children.museum
|
||||
childrens.museum
|
||||
childrensgarden.museum
|
||||
chiropractic.museum
|
||||
chocolate.museum
|
||||
christiansburg.museum
|
||||
cincinnati.museum
|
||||
cinema.museum
|
||||
circus.museum
|
||||
civilisation.museum
|
||||
civilization.museum
|
||||
civilwar.museum
|
||||
clinton.museum
|
||||
clock.museum
|
||||
coal.museum
|
||||
coastaldefence.museum
|
||||
cody.museum
|
||||
coldwar.museum
|
||||
collection.museum
|
||||
colonialwilliamsburg.museum
|
||||
coloradoplateau.museum
|
||||
columbia.museum
|
||||
columbus.museum
|
||||
communication.museum
|
||||
communications.museum
|
||||
community.museum
|
||||
computer.museum
|
||||
computerhistory.museum
|
||||
comunicações.museum
|
||||
contemporary.museum
|
||||
contemporaryart.museum
|
||||
convent.museum
|
||||
copenhagen.museum
|
||||
corporation.museum
|
||||
correios-e-telecomunicações.museum
|
||||
corvette.museum
|
||||
costume.museum
|
||||
countryestate.museum
|
||||
county.museum
|
||||
crafts.museum
|
||||
cranbrook.museum
|
||||
creation.museum
|
||||
cultural.museum
|
||||
culturalcenter.museum
|
||||
culture.museum
|
||||
cyber.museum
|
||||
cymru.museum
|
||||
dali.museum
|
||||
dallas.museum
|
||||
database.museum
|
||||
ddr.museum
|
||||
decorativearts.museum
|
||||
delaware.museum
|
||||
delmenhorst.museum
|
||||
denmark.museum
|
||||
depot.museum
|
||||
design.museum
|
||||
detroit.museum
|
||||
dinosaur.museum
|
||||
discovery.museum
|
||||
dolls.museum
|
||||
donostia.museum
|
||||
durham.museum
|
||||
eastafrica.museum
|
||||
eastcoast.museum
|
||||
education.museum
|
||||
educational.museum
|
||||
egyptian.museum
|
||||
eisenbahn.museum
|
||||
elburg.museum
|
||||
elvendrell.museum
|
||||
embroidery.museum
|
||||
encyclopedic.museum
|
||||
england.museum
|
||||
entomology.museum
|
||||
environment.museum
|
||||
environmentalconservation.museum
|
||||
epilepsy.museum
|
||||
essex.museum
|
||||
estate.museum
|
||||
ethnology.museum
|
||||
exeter.museum
|
||||
exhibition.museum
|
||||
family.museum
|
||||
farm.museum
|
||||
farmequipment.museum
|
||||
farmers.museum
|
||||
farmstead.museum
|
||||
field.museum
|
||||
figueres.museum
|
||||
filatelia.museum
|
||||
film.museum
|
||||
fineart.museum
|
||||
finearts.museum
|
||||
finland.museum
|
||||
flanders.museum
|
||||
florida.museum
|
||||
force.museum
|
||||
fortmissoula.museum
|
||||
fortworth.museum
|
||||
foundation.museum
|
||||
francaise.museum
|
||||
frankfurt.museum
|
||||
franziskaner.museum
|
||||
freemasonry.museum
|
||||
freiburg.museum
|
||||
fribourg.museum
|
||||
frog.museum
|
||||
fundacio.museum
|
||||
furniture.museum
|
||||
gallery.museum
|
||||
garden.museum
|
||||
gateway.museum
|
||||
geelvinck.museum
|
||||
gemological.museum
|
||||
geology.museum
|
||||
georgia.museum
|
||||
giessen.museum
|
||||
glas.museum
|
||||
glass.museum
|
||||
gorge.museum
|
||||
grandrapids.museum
|
||||
graz.museum
|
||||
guernsey.museum
|
||||
halloffame.museum
|
||||
hamburg.museum
|
||||
handson.museum
|
||||
harvestcelebration.museum
|
||||
hawaii.museum
|
||||
health.museum
|
||||
heimatunduhren.museum
|
||||
hellas.museum
|
||||
helsinki.museum
|
||||
hembygdsforbund.museum
|
||||
heritage.museum
|
||||
histoire.museum
|
||||
historical.museum
|
||||
historicalsociety.museum
|
||||
historichouses.museum
|
||||
historisch.museum
|
||||
historisches.museum
|
||||
history.museum
|
||||
historyofscience.museum
|
||||
horology.museum
|
||||
house.museum
|
||||
humanities.museum
|
||||
illustration.museum
|
||||
imageandsound.museum
|
||||
indian.museum
|
||||
indiana.museum
|
||||
indianapolis.museum
|
||||
indianmarket.museum
|
||||
intelligence.museum
|
||||
interactive.museum
|
||||
iraq.museum
|
||||
iron.museum
|
||||
isleofman.museum
|
||||
jamison.museum
|
||||
jefferson.museum
|
||||
jerusalem.museum
|
||||
jewelry.museum
|
||||
jewish.museum
|
||||
jewishart.museum
|
||||
jfk.museum
|
||||
journalism.museum
|
||||
judaica.museum
|
||||
judygarland.museum
|
||||
juedisches.museum
|
||||
juif.museum
|
||||
karate.museum
|
||||
karikatur.museum
|
||||
kids.museum
|
||||
koebenhavn.museum
|
||||
koeln.museum
|
||||
kunst.museum
|
||||
kunstsammlung.museum
|
||||
kunstunddesign.museum
|
||||
labor.museum
|
||||
labour.museum
|
||||
lajolla.museum
|
||||
lancashire.museum
|
||||
landes.museum
|
||||
lans.museum
|
||||
läns.museum
|
||||
larsson.museum
|
||||
lewismiller.museum
|
||||
lincoln.museum
|
||||
linz.museum
|
||||
living.museum
|
||||
livinghistory.museum
|
||||
localhistory.museum
|
||||
london.museum
|
||||
losangeles.museum
|
||||
louvre.museum
|
||||
loyalist.museum
|
||||
lucerne.museum
|
||||
luxembourg.museum
|
||||
luzern.museum
|
||||
mad.museum
|
||||
madrid.museum
|
||||
mallorca.museum
|
||||
manchester.museum
|
||||
mansion.museum
|
||||
mansions.museum
|
||||
manx.museum
|
||||
marburg.museum
|
||||
maritime.museum
|
||||
maritimo.museum
|
||||
maryland.museum
|
||||
marylhurst.museum
|
||||
media.museum
|
||||
medical.museum
|
||||
medizinhistorisches.museum
|
||||
meeres.museum
|
||||
memorial.museum
|
||||
mesaverde.museum
|
||||
michigan.museum
|
||||
midatlantic.museum
|
||||
military.museum
|
||||
mill.museum
|
||||
miners.museum
|
||||
mining.museum
|
||||
minnesota.museum
|
||||
missile.museum
|
||||
missoula.museum
|
||||
modern.museum
|
||||
moma.museum
|
||||
money.museum
|
||||
monmouth.museum
|
||||
monticello.museum
|
||||
montreal.museum
|
||||
moscow.museum
|
||||
motorcycle.museum
|
||||
muenchen.museum
|
||||
muenster.museum
|
||||
mulhouse.museum
|
||||
muncie.museum
|
||||
museet.museum
|
||||
museumcenter.museum
|
||||
museumvereniging.museum
|
||||
music.museum
|
||||
national.museum
|
||||
nationalfirearms.museum
|
||||
nationalheritage.museum
|
||||
nativeamerican.museum
|
||||
naturalhistory.museum
|
||||
naturalhistorymuseum.museum
|
||||
naturalsciences.museum
|
||||
nature.museum
|
||||
naturhistorisches.museum
|
||||
natuurwetenschappen.museum
|
||||
naumburg.museum
|
||||
naval.museum
|
||||
nebraska.museum
|
||||
neues.museum
|
||||
newhampshire.museum
|
||||
newjersey.museum
|
||||
newmexico.museum
|
||||
newport.museum
|
||||
newspaper.museum
|
||||
newyork.museum
|
||||
niepce.museum
|
||||
norfolk.museum
|
||||
north.museum
|
||||
nrw.museum
|
||||
nyc.museum
|
||||
nyny.museum
|
||||
oceanographic.museum
|
||||
oceanographique.museum
|
||||
omaha.museum
|
||||
online.museum
|
||||
ontario.museum
|
||||
openair.museum
|
||||
oregon.museum
|
||||
oregontrail.museum
|
||||
otago.museum
|
||||
oxford.museum
|
||||
pacific.museum
|
||||
paderborn.museum
|
||||
palace.museum
|
||||
paleo.museum
|
||||
palmsprings.museum
|
||||
panama.museum
|
||||
paris.museum
|
||||
pasadena.museum
|
||||
pharmacy.museum
|
||||
philadelphia.museum
|
||||
philadelphiaarea.museum
|
||||
philately.museum
|
||||
phoenix.museum
|
||||
photography.museum
|
||||
pilots.museum
|
||||
pittsburgh.museum
|
||||
planetarium.museum
|
||||
plantation.museum
|
||||
plants.museum
|
||||
plaza.museum
|
||||
portal.museum
|
||||
portland.museum
|
||||
portlligat.museum
|
||||
posts-and-telecommunications.museum
|
||||
preservation.museum
|
||||
presidio.museum
|
||||
press.museum
|
||||
project.museum
|
||||
public.museum
|
||||
pubol.museum
|
||||
quebec.museum
|
||||
railroad.museum
|
||||
railway.museum
|
||||
research.museum
|
||||
resistance.museum
|
||||
riodejaneiro.museum
|
||||
rochester.museum
|
||||
rockart.museum
|
||||
roma.museum
|
||||
russia.museum
|
||||
saintlouis.museum
|
||||
salem.museum
|
||||
salvadordali.museum
|
||||
salzburg.museum
|
||||
sandiego.museum
|
||||
sanfrancisco.museum
|
||||
santabarbara.museum
|
||||
santacruz.museum
|
||||
santafe.museum
|
||||
saskatchewan.museum
|
||||
satx.museum
|
||||
savannahga.museum
|
||||
schlesisches.museum
|
||||
schoenbrunn.museum
|
||||
schokoladen.museum
|
||||
school.museum
|
||||
schweiz.museum
|
||||
science.museum
|
||||
scienceandhistory.museum
|
||||
scienceandindustry.museum
|
||||
sciencecenter.museum
|
||||
sciencecenters.museum
|
||||
science-fiction.museum
|
||||
sciencehistory.museum
|
||||
sciences.museum
|
||||
sciencesnaturelles.museum
|
||||
scotland.museum
|
||||
seaport.museum
|
||||
settlement.museum
|
||||
settlers.museum
|
||||
shell.museum
|
||||
sherbrooke.museum
|
||||
sibenik.museum
|
||||
silk.museum
|
||||
ski.museum
|
||||
skole.museum
|
||||
society.museum
|
||||
sologne.museum
|
||||
soundandvision.museum
|
||||
southcarolina.museum
|
||||
southwest.museum
|
||||
space.museum
|
||||
spy.museum
|
||||
square.museum
|
||||
stadt.museum
|
||||
stalbans.museum
|
||||
starnberg.museum
|
||||
state.museum
|
||||
stateofdelaware.museum
|
||||
station.museum
|
||||
steam.museum
|
||||
steiermark.museum
|
||||
stjohn.museum
|
||||
stockholm.museum
|
||||
stpetersburg.museum
|
||||
stuttgart.museum
|
||||
suisse.museum
|
||||
surgeonshall.museum
|
||||
surrey.museum
|
||||
svizzera.museum
|
||||
sweden.museum
|
||||
sydney.museum
|
||||
tank.museum
|
||||
tcm.museum
|
||||
technology.museum
|
||||
telekommunikation.museum
|
||||
television.museum
|
||||
texas.museum
|
||||
textile.museum
|
||||
theater.museum
|
||||
time.museum
|
||||
timekeeping.museum
|
||||
topology.museum
|
||||
torino.museum
|
||||
touch.museum
|
||||
town.museum
|
||||
transport.museum
|
||||
tree.museum
|
||||
trolley.museum
|
||||
trust.museum
|
||||
trustee.museum
|
||||
uhren.museum
|
||||
ulm.museum
|
||||
undersea.museum
|
||||
university.museum
|
||||
usa.museum
|
||||
usantiques.museum
|
||||
usarts.museum
|
||||
uscountryestate.museum
|
||||
usculture.museum
|
||||
usdecorativearts.museum
|
||||
usgarden.museum
|
||||
ushistory.museum
|
||||
ushuaia.museum
|
||||
uslivinghistory.museum
|
||||
utah.museum
|
||||
uvic.museum
|
||||
valley.museum
|
||||
vantaa.museum
|
||||
versailles.museum
|
||||
viking.museum
|
||||
village.museum
|
||||
virginia.museum
|
||||
virtual.museum
|
||||
virtuel.museum
|
||||
vlaanderen.museum
|
||||
volkenkunde.museum
|
||||
wales.museum
|
||||
wallonie.museum
|
||||
war.museum
|
||||
washingtondc.museum
|
||||
watchandclock.museum
|
||||
watch-and-clock.museum
|
||||
western.museum
|
||||
westfalen.museum
|
||||
whaling.museum
|
||||
wildlife.museum
|
||||
williamsburg.museum
|
||||
windmill.museum
|
||||
workshop.museum
|
||||
york.museum
|
||||
yorkshire.museum
|
||||
yosemite.museum
|
||||
youth.museum
|
||||
zoological.museum
|
||||
zoology.museum
|
||||
ירושלים.museum
|
||||
иком.museum
|
||||
|
||||
// mv : https://en.wikipedia.org/wiki/.mv
|
||||
// "mv" included because, contra Wikipedia, google.mv exists.
|
||||
|
@ -5861,7 +5313,7 @@ zarow.pl
|
|||
zgora.pl
|
||||
zgorzelec.pl
|
||||
|
||||
// pm : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
|
||||
// pm : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
pm
|
||||
|
||||
// pn : http://www.government.pn/PnRegistry/policies.htm
|
||||
|
@ -5959,7 +5411,7 @@ net.qa
|
|||
org.qa
|
||||
sch.qa
|
||||
|
||||
// re : http://www.afnic.re/obtenir/chartes/nommage-re/annexe-descriptifs
|
||||
// re : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
re
|
||||
asso.re
|
||||
com.re
|
||||
|
@ -6216,7 +5668,7 @@ td
|
|||
// http://www.telnic.org/
|
||||
tel
|
||||
|
||||
// tf : https://en.wikipedia.org/wiki/.tf
|
||||
// tf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
tf
|
||||
|
||||
// tg : https://en.wikipedia.org/wiki/.tg
|
||||
|
@ -6835,7 +6287,7 @@ edu.vu
|
|||
net.vu
|
||||
org.vu
|
||||
|
||||
// wf : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
|
||||
// wf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
wf
|
||||
|
||||
// ws : https://en.wikipedia.org/wiki/.ws
|
||||
|
@ -6847,7 +6299,7 @@ org.ws
|
|||
gov.ws
|
||||
edu.ws
|
||||
|
||||
// yt : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
|
||||
// yt : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
|
||||
yt
|
||||
|
||||
// IDN ccTLDs
|
||||
|
@ -13459,6 +12911,10 @@ qoto.io
|
|||
// Submitted by Xavier De Cock <xdecock@gmail.com>
|
||||
qualifioapp.com
|
||||
|
||||
// Quality Unit: https://qualityunit.com
|
||||
// Submitted by Vasyl Tsalko <vtsalko@qualityunit.com>
|
||||
ladesk.com
|
||||
|
||||
// QuickBackend: https://www.quickbackend.com
|
||||
// Submitted by Dani Biro <dani@pymet.com>
|
||||
qbuser.com
|
||||
|
|
|
@ -268,7 +268,7 @@ public class ActivityDmarc extends ActivityBase {
|
|||
boolean valid = false;
|
||||
if (spf != null)
|
||||
for (Pair<String, DnsHelper.DnsRecord> p : spf) {
|
||||
for (String ip : p.second.name.split("\\s+")) {
|
||||
for (String ip : p.second.response.split("\\s+")) {
|
||||
ip = ip.toLowerCase(Locale.ROOT);
|
||||
if (ip.startsWith("ip4:") || ip.startsWith("ip6:")) {
|
||||
String[] net = ip.substring(4).split("/");
|
||||
|
@ -290,15 +290,15 @@ public class ActivityDmarc extends ActivityBase {
|
|||
for (DnsHelper.DnsRecord mx : mxs) {
|
||||
List<DnsHelper.DnsRecord> as = new ArrayList<>();
|
||||
try {
|
||||
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.name, "a")));
|
||||
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.response, "a")));
|
||||
} catch (UnknownHostException ignored) {
|
||||
}
|
||||
try {
|
||||
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.name, "aaaa")));
|
||||
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.response, "aaaa")));
|
||||
} catch (UnknownHostException ignored) {
|
||||
}
|
||||
for (DnsHelper.DnsRecord a : as)
|
||||
if (text.equals(a.name)) {
|
||||
if (text.equals(a.response)) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
|
@ -431,7 +431,7 @@ public class ActivityDmarc extends ActivityBase {
|
|||
spf = lookupSpf(context, lastDomain, extra);
|
||||
for (Pair<String, DnsHelper.DnsRecord> p : spf) {
|
||||
ssb.append(p.first).append(' ')
|
||||
.append(p.second.name).append("\n");
|
||||
.append(p.second.response).append("\n");
|
||||
if (start == null) {
|
||||
start = ssb.length();
|
||||
ssb.append("\n");
|
||||
|
@ -455,7 +455,7 @@ public class ActivityDmarc extends ActivityBase {
|
|||
} catch (UnknownHostException ignored) {
|
||||
}
|
||||
for (DnsHelper.DnsRecord r : records)
|
||||
ssb.append(r.name).append("\n");
|
||||
ssb.append(r.response).append("\n");
|
||||
ssb.append("\n");
|
||||
}
|
||||
}
|
||||
|
@ -528,9 +528,9 @@ public class ActivityDmarc extends ActivityBase {
|
|||
ssb.append(domain).append('=')
|
||||
.append(Integer.toString(records.length)).append('\n');
|
||||
for (DnsHelper.DnsRecord r : records)
|
||||
if (r.name.contains("spf")) {
|
||||
if (r.response.contains("spf")) {
|
||||
result.add(new Pair<>(domain, r));
|
||||
for (String part : r.name.split("\\s+"))
|
||||
for (String part : r.response.split("\\s+"))
|
||||
if (part.toLowerCase(Locale.ROOT).startsWith("include:")) {
|
||||
String sub = part.substring("include:".length());
|
||||
result.addAll(lookupSpf(context, sub, ssb));
|
||||
|
|
|
@ -2839,8 +2839,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onScrollChange(int scrollX, int scrollY) {
|
||||
properties.setPosition(message.id, new Pair<Integer, Integer>(scrollX, scrollY));
|
||||
public void onScrollChange(int dx, int dy, int scrollX, int scrollY) {
|
||||
properties.setPosition(message.id, new Pair<>(dx, dy), new Pair<>(scrollX, scrollY));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4500,7 +4500,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
properties.setSize(message.id, null);
|
||||
properties.setHeight(message.id, null);
|
||||
properties.setPosition(message.id, null);
|
||||
properties.setPosition(message.id, null, null);
|
||||
|
||||
if (itemId == R.string.title_fit_width && wvBody instanceof WebView)
|
||||
((WebView) wvBody).getSettings().setLoadWithOverviewMode(enabled);
|
||||
|
@ -5371,7 +5371,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
properties.setSize(message.id, null);
|
||||
properties.setHeight(message.id, null);
|
||||
properties.setPosition(message.id, null);
|
||||
properties.setPosition(message.id, null, null);
|
||||
|
||||
if (full)
|
||||
setupTools(message, false, false);
|
||||
|
@ -6589,7 +6589,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
public boolean onMenuItemClick(MenuItem item) {
|
||||
properties.setSize(message.id, null);
|
||||
properties.setHeight(message.id, null);
|
||||
properties.setPosition(message.id, null);
|
||||
properties.setPosition(message.id, null, null);
|
||||
|
||||
args.putString("charset", item.getIntent().getStringExtra("charset"));
|
||||
|
||||
|
@ -6642,7 +6642,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private void onMenuAlt(TupleMessageEx message) {
|
||||
properties.setSize(message.id, null);
|
||||
properties.setHeight(message.id, null);
|
||||
properties.setPosition(message.id, null);
|
||||
properties.setPosition(message.id, null, null);
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", message.id);
|
||||
|
@ -8571,7 +8571,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
int getHeight(long id, int defaultHeight);
|
||||
|
||||
void setPosition(long id, Pair<Integer, Integer> position);
|
||||
void setPosition(long id, Pair<Integer, Integer> delta, Pair<Integer, Integer> position);
|
||||
|
||||
Pair<Integer, Integer> getPosition(long id);
|
||||
|
||||
|
|
|
@ -3014,7 +3014,7 @@ class Core {
|
|||
imessages.length > 0 && folder.last_sync_count != null &&
|
||||
imessages.length == folder.last_sync_count) {
|
||||
// Check if last message known as new messages indicator
|
||||
MessageHelper helper = new MessageHelper((MimeMessage) imessages[imessages.length - 1], context);
|
||||
MessageHelper helper = new MessageHelper((MimeMessage) imessages[reversed ? 0 : imessages.length - 1], context);
|
||||
String msgid = helper.getPOP3MessageID();
|
||||
if (msgid != null) {
|
||||
int count = db.message().countMessageByMsgId(folder.id, msgid, true);
|
||||
|
@ -3025,9 +3025,33 @@ class Core {
|
|||
}
|
||||
}
|
||||
|
||||
// Index IDs
|
||||
int flagged = 0;
|
||||
Map<String, TupleUidl> uidlTuple = new HashMap<>();
|
||||
Map<String, TupleUidl> msgIdTuple = new HashMap<>();
|
||||
for (TupleUidl id : ids) {
|
||||
if (id.ui_flagged && !id.ui_hide)
|
||||
flagged++;
|
||||
|
||||
if (id.uidl != null) {
|
||||
if (uidlTuple.containsKey(id.uidl))
|
||||
Log.w(account.name + " POP duplicate uidl/msgid=" + id.uidl + "/" + id.msgid);
|
||||
uidlTuple.put(id.uidl, id);
|
||||
}
|
||||
|
||||
if (id.msgid != null) {
|
||||
if (msgIdTuple.containsKey(id.msgid))
|
||||
Log.w(account.name + " POP duplicate msgid/uidl=" + id.msgid + "/" + id.uidl);
|
||||
msgIdTuple.put(id.msgid, id);
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.min(max + flagged, imessages.length);
|
||||
|
||||
EntityLog.log(context, account.name + " POP" +
|
||||
" device=" + ids.size() +
|
||||
" server=" + imessages.length +
|
||||
" flagged=" + flagged +
|
||||
" max=" + max + "/" + account.max_messages +
|
||||
" reversed=" + reversed +
|
||||
" last=" + folder.last_sync_count +
|
||||
|
@ -3035,24 +3059,6 @@ class Core {
|
|||
" uidl=" + hasUidl);
|
||||
|
||||
if (sync) {
|
||||
// Index IDs
|
||||
Map<String, TupleUidl> uidlTuple = new HashMap<>();
|
||||
for (TupleUidl id : ids) {
|
||||
if (id.uidl != null) {
|
||||
if (uidlTuple.containsKey(id.uidl))
|
||||
Log.w(account.name + " POP duplicate uidl/msgid=" + id.uidl + "/" + id.msgid);
|
||||
uidlTuple.put(id.uidl, id);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, TupleUidl> msgIdTuple = new HashMap<>();
|
||||
for (TupleUidl id : ids)
|
||||
if (id.msgid != null) {
|
||||
if (msgIdTuple.containsKey(id.msgid))
|
||||
Log.w(account.name + " POP duplicate msgid/uidl=" + id.msgid + "/" + id.uidl);
|
||||
msgIdTuple.put(id.msgid, id);
|
||||
}
|
||||
|
||||
// Fetch UIDLs
|
||||
if (hasUidl) {
|
||||
FetchProfile ifetch = new FetchProfile();
|
||||
|
@ -3427,8 +3433,9 @@ class Core {
|
|||
int hidden = db.message().setMessagesUiHide(folder.id, Math.abs(account.max_messages));
|
||||
int deleted = db.message().deleteMessagesKeep(folder.id, Math.abs(account.max_messages) + 100);
|
||||
EntityLog.log(context, account.name + " POP" +
|
||||
" cleanup max=" + account.max_messages + "" +
|
||||
" hidden=" + hidden + " deleted=" + deleted);
|
||||
" cleanup max=" + account.max_messages +
|
||||
" hidden=" + hidden +
|
||||
" deleted=" + deleted);
|
||||
}
|
||||
|
||||
folder.last_sync_count = imessages.length;
|
||||
|
@ -5256,6 +5263,13 @@ class Core {
|
|||
" ignored=" + message.ui_ignored +
|
||||
" hide=" + message.ui_hide);
|
||||
else {
|
||||
// Prevent reappearing notifications
|
||||
EntityMessage msg = db.message().getMessage(message.id);
|
||||
if (msg == null || msg.ui_ignored) {
|
||||
Log.i("Notify skip id=" + message.id + " msg=" + (msg != null));
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer current = newMessages.get(group);
|
||||
newMessages.put(group, current == null ? 1 : current + 1);
|
||||
|
||||
|
@ -5289,11 +5303,6 @@ class Core {
|
|||
remove.remove(id);
|
||||
Log.i("Notify existing=" + id);
|
||||
} else {
|
||||
EntityMessage msg = db.message().getMessage(message.id);
|
||||
if (msg == null || msg.ui_ignored) {
|
||||
Log.i("Notify skip id=" + message.id + " msg=" + (msg != null));
|
||||
continue;
|
||||
}
|
||||
boolean existing = remove.contains(-id);
|
||||
if (existing) {
|
||||
if (message.content && notify_preview) {
|
||||
|
|
|
@ -74,6 +74,11 @@ public interface DaoAnswer {
|
|||
" AND (:favorite OR NOT favorite)")
|
||||
Integer getAnswerCount(boolean favorite);
|
||||
|
||||
@Query("SELECT DISTINCT `group` FROM answer" +
|
||||
" WHERE NOT `group` IS NULL" +
|
||||
" ORDER by `group` COLLATE NOCASE")
|
||||
List<String> getGroups();
|
||||
|
||||
@Insert
|
||||
long insertAnswer(EntityAnswer answer);
|
||||
|
||||
|
|
|
@ -789,6 +789,7 @@ public interface DaoMessage {
|
|||
@Transaction
|
||||
@Query("UPDATE message SET ui_hide = 1" +
|
||||
" WHERE folder = :folder" +
|
||||
" AND NOT ui_flagged" +
|
||||
" AND id NOT IN (" +
|
||||
" SELECT id FROM message" +
|
||||
" WHERE folder = :folder" +
|
||||
|
@ -1015,6 +1016,7 @@ public interface DaoMessage {
|
|||
@Transaction
|
||||
@Query("DELETE FROM message" +
|
||||
" WHERE folder = :folder" +
|
||||
" AND NOT ui_flagged" +
|
||||
" AND id NOT IN (" +
|
||||
" SELECT id FROM message" +
|
||||
" WHERE folder = :folder" +
|
||||
|
|
|
@ -47,6 +47,12 @@ public interface DaoRule {
|
|||
" WHERE rule.id = :id")
|
||||
TupleRuleEx getRule(long id);
|
||||
|
||||
@Query("SELECT rule.* FROM rule" +
|
||||
" JOIN folder ON folder.id = rule.folder" +
|
||||
" WHERE folder.account = :account" +
|
||||
" AND rule.name = :name")
|
||||
List<EntityRule> getRuleByName(long account, String name);
|
||||
|
||||
@Query("SELECT * FROM rule WHERE uuid = :uuid")
|
||||
EntityRule getRuleByUUID(String uuid);
|
||||
|
||||
|
@ -56,6 +62,11 @@ public interface DaoRule {
|
|||
" WHERE rule.folder = :folder")
|
||||
LiveData<List<TupleRuleEx>> liveRules(long folder);
|
||||
|
||||
@Query("SELECT DISTINCT `group` FROM rule" +
|
||||
" WHERE NOT `group` IS NULL" +
|
||||
" ORDER by `group` COLLATE NOCASE")
|
||||
List<String> getGroups();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM rule")
|
||||
int countTotal();
|
||||
|
||||
|
|
|
@ -210,7 +210,7 @@ public class DnsHelper {
|
|||
result.add(new DnsRecord(soa.getHost().toString(true)));
|
||||
} else if (record instanceof SRVRecord) {
|
||||
SRVRecord srv = (SRVRecord) record;
|
||||
result.add(new DnsRecord(srv.getTarget().toString(true), srv.getPort()));
|
||||
result.add(new DnsRecord(srv.getTarget().toString(true), srv.getPort(), srv.getPriority(), srv.getWeight()));
|
||||
} else if (record instanceof TXTRecord) {
|
||||
TXTRecord txt = (TXTRecord) record;
|
||||
for (Object content : txt.getStrings()) {
|
||||
|
@ -227,7 +227,7 @@ public class DnsHelper {
|
|||
slash = text.indexOf('\\', i);
|
||||
}
|
||||
if (result.size() > 0)
|
||||
result.get(0).name += text;
|
||||
result.get(0).response += text;
|
||||
else
|
||||
result.add(new DnsRecord(text, 0));
|
||||
}
|
||||
|
@ -241,6 +241,9 @@ public class DnsHelper {
|
|||
throw new IllegalArgumentException(record.getClass().getName());
|
||||
}
|
||||
|
||||
for (DnsRecord record : result)
|
||||
record.query = name;
|
||||
|
||||
return result.toArray(new DnsRecord[0]);
|
||||
} catch (TextParseException ex) {
|
||||
Log.e(ex);
|
||||
|
@ -283,16 +286,32 @@ public class DnsHelper {
|
|||
}
|
||||
|
||||
static class DnsRecord {
|
||||
String name;
|
||||
String query;
|
||||
String response;
|
||||
Integer port;
|
||||
Integer priority;
|
||||
Integer weight;
|
||||
|
||||
DnsRecord(String name) {
|
||||
this.name = name;
|
||||
DnsRecord(String response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
DnsRecord(String name, int port) {
|
||||
this.name = name;
|
||||
DnsRecord(String response, int port) {
|
||||
this.response = response;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
DnsRecord(String response, int port, int priority, int weight) {
|
||||
this.response = response;
|
||||
this.port = port;
|
||||
this.priority = priority;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return query + "=" + response + ":" + port + " " + priority + "/" + weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
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-2023 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ActionMode;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.textclassifier.TextClassifier;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
|
||||
|
||||
public class EditTextAutoComplete extends AppCompatAutoCompleteTextView {
|
||||
public EditTextAutoComplete(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EditTextAutoComplete(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EditTextAutoComplete(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelection(int index) {
|
||||
try {
|
||||
super.setSelection(index);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelection(int start, int stop) {
|
||||
try {
|
||||
super.setSelection(start, stop);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(CharSequence text, BufferType type) {
|
||||
try {
|
||||
super.setText(text, type);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
try {
|
||||
return super.onPreDraw();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
try {
|
||||
super.onDraw(canvas);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
try {
|
||||
return super.dispatchTouchEvent(event);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchWindowFocusChanged(boolean hasFocus) {
|
||||
try {
|
||||
super.dispatchWindowFocusChanged(hasFocus);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
try {
|
||||
return super.onTouchEvent(event);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
try {
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
try {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performClick() {
|
||||
try {
|
||||
return super.performClick();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performLongClick() {
|
||||
try {
|
||||
return super.performLongClick();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionMode startActionMode(ActionMode.Callback callback) {
|
||||
try {
|
||||
return super.startActionMode(callback);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
|
||||
try {
|
||||
return super.startActionMode(callback, type);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public TextClassifier getTextClassifier() {
|
||||
if (BuildConfig.DEBUG /*|| Helper.isSamsung()*/)
|
||||
return TextClassifier.NO_OP;
|
||||
else
|
||||
return super.getTextClassifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTextContextMenuItem(int id) {
|
||||
try {
|
||||
return super.onTextContextMenuItem(id);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -427,7 +427,7 @@ public class EmailProvider implements Parcelable {
|
|||
for (EmailProvider provider : providers)
|
||||
if (provider.mx != null)
|
||||
for (String mx : provider.mx)
|
||||
if (record.name.matches(mx))
|
||||
if (record.response.matches(mx))
|
||||
return Arrays.asList(provider);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
|
@ -477,8 +477,8 @@ public class EmailProvider implements Parcelable {
|
|||
}
|
||||
|
||||
for (DnsHelper.DnsRecord record : records)
|
||||
if (!TextUtils.isEmpty(record.name)) {
|
||||
String target = record.name.toLowerCase(Locale.ROOT);
|
||||
if (!TextUtils.isEmpty(record.response)) {
|
||||
String target = record.response.toLowerCase(Locale.ROOT);
|
||||
EntityLog.log(context, "MX target=" + target);
|
||||
|
||||
for (EmailProvider provider : providers) {
|
||||
|
@ -510,7 +510,7 @@ public class EmailProvider implements Parcelable {
|
|||
|
||||
for (DnsHelper.DnsRecord record : records)
|
||||
try {
|
||||
String target = record.name.toLowerCase(Locale.ROOT);
|
||||
String target = record.response.toLowerCase(Locale.ROOT);
|
||||
InetAddress.getByName(target);
|
||||
|
||||
EmailProvider mx1 = new EmailProvider(domain);
|
||||
|
@ -857,56 +857,81 @@ public class EmailProvider implements Parcelable {
|
|||
EmailProvider provider = new EmailProvider(domain);
|
||||
|
||||
if (discover == Discover.ALL || discover == Discover.IMAP) {
|
||||
try {
|
||||
// Identifies an IMAP server where TLS is initiated directly upon connection to the IMAP server.
|
||||
intf.onStatus("SRV imaps " + domain);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_imaps._tcp." + domain, "srv");
|
||||
if (records.length == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
// ... service is not supported at all at a particular domain by setting the target of an SRV RR to "."
|
||||
provider.imap.score = 50;
|
||||
provider.imap.host = records[0].name;
|
||||
provider.imap.port = records[0].port;
|
||||
provider.imap.starttls = false;
|
||||
EntityLog.log(context, "_imaps._tcp." + domain + "=" + provider.imap);
|
||||
} catch (UnknownHostException ignored) {
|
||||
// Identifies an IMAP server that MAY ... require the MUA to use the "STARTTLS" command
|
||||
intf.onStatus("SRV imap " + domain);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_imap._tcp." + domain, "srv");
|
||||
if (records.length == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
provider.imap.score = 50;
|
||||
provider.imap.host = records[0].name;
|
||||
provider.imap.port = records[0].port;
|
||||
provider.imap.starttls = (provider.imap.port == 143);
|
||||
EntityLog.log(context, "_imap._tcp." + domain + "=" + provider.imap);
|
||||
}
|
||||
intf.onStatus("SRV imap " + domain);
|
||||
|
||||
// Identifies an IMAP server where TLS is initiated directly upon connection to the IMAP server.
|
||||
List<DnsHelper.DnsRecord> list = new ArrayList<>();
|
||||
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_imap._tcp." + domain, "srv")));
|
||||
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_imaps._tcp." + domain, "srv")));
|
||||
|
||||
// ... service is not supported at all at a particular domain by setting the target of an SRV RR to "."
|
||||
for (DnsHelper.DnsRecord record : new ArrayList<>(list))
|
||||
if (TextUtils.isEmpty(record.response) || ".".equals(record.response))
|
||||
list.remove(record);
|
||||
|
||||
if (list.size() == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
|
||||
Collections.sort(list, new Comparator<DnsHelper.DnsRecord>() {
|
||||
@Override
|
||||
public int compare(DnsHelper.DnsRecord d1, DnsHelper.DnsRecord d2) {
|
||||
int p = -Integer.compare(d1.priority, d2.priority);
|
||||
if (p != 0)
|
||||
return p;
|
||||
int w = -Integer.compare(d1.weight, d2.weight);
|
||||
if (w != 0)
|
||||
return w;
|
||||
return -Boolean.compare(d1.query.startsWith("_imaps._tcp."), d2.query.startsWith("_imaps._tcp."));
|
||||
}
|
||||
});
|
||||
|
||||
DnsHelper.DnsRecord pref = list.get(0);
|
||||
|
||||
provider.imap.score = 50;
|
||||
provider.imap.host = pref.response;
|
||||
provider.imap.port = pref.port;
|
||||
provider.imap.starttls = (!pref.query.startsWith("_imaps._tcp.") && pref.port == 143);
|
||||
EntityLog.log(context, pref.query + "=" + provider.imap);
|
||||
}
|
||||
|
||||
if (discover == Discover.ALL || discover == Discover.SMTP)
|
||||
try {
|
||||
// Note that this covers connections both with and without Transport Layer Security (TLS)
|
||||
intf.onStatus("SRV smtp " + domain);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_submission._tcp." + domain, "srv");
|
||||
if (records.length == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
provider.smtp.score = 50;
|
||||
provider.smtp.host = records[0].name;
|
||||
provider.smtp.port = records[0].port;
|
||||
provider.smtp.starttls = (provider.smtp.port == 587);
|
||||
EntityLog.log(context, "_submission._tcp." + domain + "=" + provider.smtp);
|
||||
} catch (UnknownHostException ignored) {
|
||||
// https://tools.ietf.org/html/rfc8314
|
||||
intf.onStatus("SRV smtps " + domain);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_submissions._tcp." + domain, "srv");
|
||||
if (records.length == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
provider.smtp.score = 50;
|
||||
provider.smtp.host = records[0].name;
|
||||
provider.smtp.port = records[0].port;
|
||||
provider.smtp.starttls = false;
|
||||
EntityLog.log(context, "_submissions._tcp." + domain + "=" + provider.smtp);
|
||||
}
|
||||
if (discover == Discover.ALL || discover == Discover.SMTP) {
|
||||
intf.onStatus("SRV smtp " + domain);
|
||||
// https://tools.ietf.org/html/rfc8314
|
||||
|
||||
List<DnsHelper.DnsRecord> list = new ArrayList<>();
|
||||
// Note that this covers connections both with and without Transport Layer Security (TLS)
|
||||
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_submission._tcp." + domain, "srv")));
|
||||
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_submissions._tcp." + domain, "srv")));
|
||||
|
||||
for (DnsHelper.DnsRecord record : new ArrayList<>(list))
|
||||
if (TextUtils.isEmpty(record.response) || ".".equals(record.response))
|
||||
list.remove(record);
|
||||
|
||||
if (list.size() == 0)
|
||||
throw new UnknownHostException(domain);
|
||||
|
||||
Collections.sort(list, new Comparator<DnsHelper.DnsRecord>() {
|
||||
@Override
|
||||
public int compare(DnsHelper.DnsRecord d1, DnsHelper.DnsRecord d2) {
|
||||
int p = -Integer.compare(d1.priority, d2.priority);
|
||||
if (p != 0)
|
||||
return p;
|
||||
int w = -Integer.compare(d1.weight, d2.weight);
|
||||
if (w != 0)
|
||||
return w;
|
||||
// submission is being preferred
|
||||
return -Boolean.compare(d1.query.startsWith("_submission._tcp."), d2.query.startsWith("_submission._tcp."));
|
||||
}
|
||||
});
|
||||
|
||||
DnsHelper.DnsRecord pref = list.get(0);
|
||||
|
||||
provider.smtp.score = 50;
|
||||
provider.smtp.host = pref.response;
|
||||
provider.smtp.port = pref.port;
|
||||
provider.smtp.starttls = (!pref.query.startsWith("_submissions._tcp.") && pref.port == 587);
|
||||
EntityLog.log(context, pref.query + "=" + provider.smtp);
|
||||
}
|
||||
|
||||
provider.validate();
|
||||
|
||||
|
|
|
@ -206,6 +206,7 @@ public class EntityFolder extends EntityOrder implements Serializable {
|
|||
put("archief", new TypeScore(EntityFolder.ARCHIVE, 100)); // Dutch
|
||||
put("Архив", new TypeScore(EntityFolder.ARCHIVE, 100));
|
||||
put("Wszystkie", new TypeScore(EntityFolder.ARCHIVE, 100)); // Polish
|
||||
put("Arkiv", new TypeScore(EntityFolder.ARCHIVE, 100)); // Norwegian
|
||||
|
||||
put("draft", new TypeScore(EntityFolder.DRAFTS, 100));
|
||||
put("concept", new TypeScore(EntityFolder.DRAFTS, 100));
|
||||
|
|
|
@ -42,6 +42,8 @@ import android.view.MenuInflater;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
|
@ -58,13 +60,14 @@ import com.google.android.material.snackbar.Snackbar;
|
|||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FragmentAnswer extends FragmentBase {
|
||||
private ViewGroup view;
|
||||
private EditText etName;
|
||||
private EditText etLabel;
|
||||
private EditText etGroup;
|
||||
private AutoCompleteTextView etGroup;
|
||||
private CheckBox cbStandard;
|
||||
private CheckBox cbReceipt;
|
||||
private CheckBox cbFavorite;
|
||||
|
@ -78,6 +81,8 @@ public class FragmentAnswer extends FragmentBase {
|
|||
private ContentLoadingProgressBar pbWait;
|
||||
private Group grpReady;
|
||||
|
||||
private ArrayAdapter<String> adapterGroup;
|
||||
|
||||
private long id = -1;
|
||||
private long copy = -1;
|
||||
|
||||
|
@ -133,6 +138,10 @@ public class FragmentAnswer extends FragmentBase {
|
|||
pbWait = view.findViewById(R.id.pbWait);
|
||||
grpReady = view.findViewById(R.id.grpReady);
|
||||
|
||||
adapterGroup = new ArrayAdapter<>(getContext(), R.layout.spinner_item1_dropdown, android.R.id.text1);
|
||||
etGroup.setThreshold(1);
|
||||
etGroup.setAdapter(adapterGroup);
|
||||
|
||||
btnColor.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -248,6 +257,8 @@ public class FragmentAnswer extends FragmentBase {
|
|||
args.putCharSequence("spanned", spanned);
|
||||
}
|
||||
|
||||
args.putStringArrayList("groups", new ArrayList<>(db.answer().getGroups()));
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
|
@ -274,6 +285,9 @@ public class FragmentAnswer extends FragmentBase {
|
|||
etText.setText((Spanned) args.getCharSequence("spanned"));
|
||||
}
|
||||
|
||||
adapterGroup.clear();
|
||||
adapterGroup.addAll(args.getStringArrayList("groups"));
|
||||
|
||||
if (answer == null)
|
||||
bottom_navigation.getMenu().removeItem(R.id.action_delete);
|
||||
|
||||
|
|
|
@ -179,13 +179,13 @@ public class FragmentDialogAccount extends FragmentDialogBase {
|
|||
if (swipes != null && swipes.size() == 1) {
|
||||
String left;
|
||||
if (swipes.get(0).swipe_left != null && swipes.get(0).swipe_left < 0)
|
||||
left = getSwipeTitle(context, (long) swipes.get(0).swipe_left);
|
||||
left = FragmentDialogSwipes.getActionTitle(context, swipes.get(0).swipe_left);
|
||||
else
|
||||
left = swipes.get(0).left_name;
|
||||
|
||||
String right;
|
||||
if (swipes.get(0).swipe_right != null && swipes.get(0).swipe_right < 0)
|
||||
right = getSwipeTitle(context, (long) swipes.get(0).swipe_right);
|
||||
right = FragmentDialogSwipes.getActionTitle(context, swipes.get(0).swipe_right);
|
||||
else
|
||||
right = swipes.get(0).right_name;
|
||||
|
||||
|
@ -196,16 +196,6 @@ public class FragmentDialogAccount extends FragmentDialogBase {
|
|||
tvRight.setText("?");
|
||||
}
|
||||
}
|
||||
|
||||
private String getSwipeTitle(Context context, long type) {
|
||||
if (type == EntityMessage.SWIPE_ACTION_SEEN)
|
||||
return context.getString(R.string.title_seen);
|
||||
|
||||
if (type == EntityMessage.SWIPE_ACTION_DELETE)
|
||||
return context.getString(R.string.title_delete_permanently);
|
||||
|
||||
return "???";
|
||||
}
|
||||
});
|
||||
|
||||
db.folder().liveSystemFolders(account).observe(this, new Observer<List<EntityFolder>>() {
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.content.Context;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
@ -46,38 +47,27 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
|
|||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_swipes, null);
|
||||
final Context context = getContext();
|
||||
View dview = LayoutInflater.from(context).inflate(R.layout.dialog_swipes, null);
|
||||
spLeft = dview.findViewById(R.id.spLeft);
|
||||
spRight = dview.findViewById(R.id.spRight);
|
||||
|
||||
adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityFolder>());
|
||||
adapter = new ArrayAdapter<>(context, R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityFolder>());
|
||||
adapter.setDropDownViewResource(R.layout.spinner_item1_dropdown);
|
||||
|
||||
spLeft.setAdapter(adapter);
|
||||
spRight.setAdapter(adapter);
|
||||
|
||||
List<EntityFolder> folders = FragmentAccount.getFolderActions(getContext());
|
||||
adapter.addAll(getFolderActions(context));
|
||||
|
||||
EntityFolder trash = new EntityFolder();
|
||||
trash.id = 2L;
|
||||
trash.name = getString(R.string.title_trash);
|
||||
folders.add(1, trash);
|
||||
|
||||
EntityFolder archive = new EntityFolder();
|
||||
archive.id = 1L;
|
||||
archive.name = getString(R.string.title_archive);
|
||||
folders.add(1, archive);
|
||||
|
||||
adapter.addAll(folders);
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
int leftPos = prefs.getInt("swipe_left_default", 2); // Trash
|
||||
int rightPos = prefs.getInt("swipe_right_default", 1); // Archive
|
||||
|
||||
spLeft.setSelection(leftPos);
|
||||
spRight.setSelection(rightPos);
|
||||
|
||||
return new AlertDialog.Builder(getContext())
|
||||
return new AlertDialog.Builder(context)
|
||||
.setView(dview)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
|
@ -97,16 +87,9 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
|
|||
.putBoolean("button_hide", true)
|
||||
.apply();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("left", left == null ? 0 : left.id);
|
||||
args.putLong("right", right == null ? 0 : right.id);
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onExecute(Context context, Bundle args) {
|
||||
long left = args.getLong("left");
|
||||
long right = args.getLong("right");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
@ -114,10 +97,7 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
|
|||
List<EntityAccount> accounts = db.account().getAccounts();
|
||||
for (EntityAccount account : accounts)
|
||||
if (account.protocol == EntityAccount.TYPE_IMAP)
|
||||
db.account().setAccountSwipes(
|
||||
account.id,
|
||||
getAction(context, left, account.id),
|
||||
getAction(context, right, account.id));
|
||||
setDefaultFolderActions(context, account.id);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
|
@ -129,30 +109,69 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
|
|||
|
||||
@Override
|
||||
protected void onExecuted(Bundle args, Void data) {
|
||||
ToastEx.makeText(getContext(), R.string.title_completed, Toast.LENGTH_LONG).show();
|
||||
ToastEx.makeText(context, R.string.title_completed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
}
|
||||
|
||||
private Long getAction(Context context, long selection, long account) {
|
||||
if (selection < 0)
|
||||
return selection;
|
||||
else if (selection == 0)
|
||||
return null;
|
||||
else {
|
||||
DB db = DB.getInstance(context);
|
||||
String type = (selection == 2 ? EntityFolder.TRASH : EntityFolder.ARCHIVE);
|
||||
EntityFolder archive = db.folder().getFolderByType(account, type);
|
||||
return (archive == null ? null : archive.id);
|
||||
}
|
||||
}
|
||||
}.execute(getContext(), getViewLifecycleOwner(), args, "dialog:swipe");
|
||||
}.execute(context, getViewLifecycleOwner(), new Bundle(), "dialog:swipe");
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
static void setDefaultFolderActions(Context context, long account) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
int leftPos = prefs.getInt("swipe_left_default", 2); // Trash
|
||||
int rightPos = prefs.getInt("swipe_right_default", 1); // Archive
|
||||
|
||||
List<EntityFolder> actions = getFolderActions(context);
|
||||
EntityFolder left = (leftPos < 0 || leftPos >= actions.size() ? null : actions.get(leftPos));
|
||||
EntityFolder right = (rightPos < 0 || rightPos >= actions.size() ? null : actions.get(rightPos));
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
db.account().setAccountSwipes(account,
|
||||
getAction(context, left == null ? 0 : left.id, account),
|
||||
getAction(context, right == null ? 0 : right.id, account));
|
||||
}
|
||||
|
||||
static List<EntityFolder> getFolderActions(Context context) {
|
||||
List<EntityFolder> folders = FragmentAccount.getFolderActions(context);
|
||||
|
||||
EntityFolder trash = new EntityFolder();
|
||||
trash.id = 2L;
|
||||
trash.name = context.getString(R.string.title_trash);
|
||||
folders.add(1, trash);
|
||||
|
||||
EntityFolder archive = new EntityFolder();
|
||||
archive.id = 1L;
|
||||
archive.name = context.getString(R.string.title_archive);
|
||||
folders.add(1, archive);
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
static String getActionTitle(Context context, long id) {
|
||||
for (EntityFolder action : getFolderActions(context))
|
||||
if (action.id.equals(id))
|
||||
return action.name;
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
private static Long getAction(Context context, long selection, long account) {
|
||||
if (selection < 0)
|
||||
return selection;
|
||||
else if (selection == 0)
|
||||
return null;
|
||||
else {
|
||||
DB db = DB.getInstance(context);
|
||||
String type = (selection == 2 ? EntityFolder.TRASH : EntityFolder.ARCHIVE);
|
||||
EntityFolder folder = db.folder().getFolderByType(account, type);
|
||||
return (folder == null ? null : folder.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -557,13 +557,8 @@ public class FragmentGmail extends FragmentBase {
|
|||
if (pop) {
|
||||
account.swipe_left = EntityMessage.SWIPE_ACTION_DELETE;
|
||||
account.swipe_right = EntityMessage.SWIPE_ACTION_SEEN;
|
||||
} else {
|
||||
for (EntityFolder folder : folders)
|
||||
if (EntityFolder.TRASH.equals(folder.type))
|
||||
account.swipe_left = folder.id;
|
||||
else if (EntityFolder.ARCHIVE.equals(folder.type))
|
||||
account.swipe_right = folder.id;
|
||||
}
|
||||
} else
|
||||
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
|
||||
|
||||
db.account().updateAccount(account);
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
|
|||
import static android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY;
|
||||
import static android.view.KeyEvent.ACTION_DOWN;
|
||||
import static android.view.KeyEvent.ACTION_UP;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
import static org.openintents.openpgp.OpenPgpSignatureResult.RESULT_KEY_MISSING;
|
||||
import static org.openintents.openpgp.OpenPgpSignatureResult.RESULT_NO_SIGNATURE;
|
||||
|
@ -2480,7 +2482,16 @@ public class FragmentMessages extends FragmentBase
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(long id, Pair<Integer, Integer> position) {
|
||||
public void setPosition(long id, Pair<Integer, Integer> delta, Pair<Integer, Integer> position) {
|
||||
if (delta != null && delta.second != 0) {
|
||||
boolean down = (delta.second > 0);
|
||||
if (scrolling != down) {
|
||||
scrolling = down;
|
||||
updateCompose();
|
||||
updateExpanded();
|
||||
}
|
||||
}
|
||||
|
||||
if (position == null)
|
||||
positions.remove(id);
|
||||
else
|
||||
|
@ -2652,6 +2663,8 @@ public class FragmentMessages extends FragmentBase
|
|||
|
||||
@Override
|
||||
public void layoutChanged() {
|
||||
if (rvMessage == null)
|
||||
return;
|
||||
rvMessage.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -4909,7 +4922,8 @@ public class FragmentMessages extends FragmentBase
|
|||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
||||
if ("pro".equals(key) || "banner_hidden".equals(key)) {
|
||||
if (grpSupport != null &&
|
||||
("pro".equals(key) || "banner_hidden".equals(key))) {
|
||||
boolean pro = ActivityBilling.isPro(getContext());
|
||||
long banner_hidden = prefs.getLong("banner_hidden", 0);
|
||||
grpSupport.setVisibility(
|
||||
|
|
|
@ -943,13 +943,8 @@ public class FragmentOAuth extends FragmentBase {
|
|||
if (pop) {
|
||||
account.swipe_left = EntityMessage.SWIPE_ACTION_DELETE;
|
||||
account.swipe_right = EntityMessage.SWIPE_ACTION_SEEN;
|
||||
} else {
|
||||
for (EntityFolder folder : folders)
|
||||
if (EntityFolder.TRASH.equals(folder.type))
|
||||
account.swipe_left = folder.id;
|
||||
else if (EntityFolder.ARCHIVE.equals(folder.type))
|
||||
account.swipe_right = folder.id;
|
||||
}
|
||||
} else
|
||||
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
|
||||
|
||||
db.account().updateAccount(account);
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import android.text.Editable;
|
|||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -49,6 +50,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
@ -78,8 +80,10 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
private TextView tvPatience;
|
||||
private TextView tvProgress;
|
||||
|
||||
private TextView tvArgument;
|
||||
private TextView tvError;
|
||||
private TextView tvErrorHint;
|
||||
private Button btnManual;
|
||||
private TextView tvInstructions;
|
||||
private Button btnHelp;
|
||||
private Button btnSupport;
|
||||
|
@ -100,6 +104,7 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
private Group grpSetup;
|
||||
private Group grpCertificate;
|
||||
private Group grpError;
|
||||
private Group grpManual;
|
||||
|
||||
private int title;
|
||||
private boolean update;
|
||||
|
@ -146,8 +151,10 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
tvPatience = view.findViewById(R.id.tvPatience);
|
||||
tvProgress = view.findViewById(R.id.tvProgress);
|
||||
|
||||
tvArgument = view.findViewById(R.id.tvArgument);
|
||||
tvError = view.findViewById(R.id.tvError);
|
||||
tvErrorHint = view.findViewById(R.id.tvErrorHint);
|
||||
btnManual = view.findViewById(R.id.btnManual);
|
||||
tvInstructions = view.findViewById(R.id.tvInstructions);
|
||||
btnHelp = view.findViewById(R.id.btnHelp);
|
||||
btnSupport = view.findViewById(R.id.btnSupport);
|
||||
|
@ -168,6 +175,7 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
grpSetup = view.findViewById(R.id.grpSetup);
|
||||
grpCertificate = view.findViewById(R.id.grpCertificate);
|
||||
grpError = view.findViewById(R.id.grpError);
|
||||
grpManual = view.findViewById(R.id.grpManual);
|
||||
|
||||
// Wire controls
|
||||
|
||||
|
@ -239,6 +247,19 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
}
|
||||
});
|
||||
|
||||
btnManual.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
|
||||
FragmentAccount fragment = new FragmentAccount();
|
||||
fragment.setArguments(new Bundle());
|
||||
FragmentTransaction fragmentTransaction = getParentFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
});
|
||||
|
||||
btnSupport.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -254,6 +275,8 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
tvPatience.setVisibility(View.GONE);
|
||||
tvProgress.setVisibility(View.GONE);
|
||||
pbSave.setVisibility(View.GONE);
|
||||
tvArgument.setVisibility(View.GONE);
|
||||
tvErrorHint.setVisibility(View.GONE);
|
||||
tvInstructions.setVisibility(View.GONE);
|
||||
tvInstructions.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
btnHelp.setVisibility(View.GONE);
|
||||
|
@ -263,6 +286,7 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
grpSetup.setVisibility(View.GONE);
|
||||
grpCertificate.setVisibility(View.GONE);
|
||||
grpError.setVisibility(View.GONE);
|
||||
grpManual.setVisibility(View.GONE);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
tilPassword.getEditText().setText(savedInstanceState.getString("fair:password"));
|
||||
|
@ -275,10 +299,43 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
}
|
||||
|
||||
private void onSave(boolean check) {
|
||||
String name = etName.getText().toString().trim();
|
||||
String email = etEmail.getText().toString().trim();
|
||||
String password = tilPassword.getEditText().getText().toString();
|
||||
String warning = null;
|
||||
if (TextUtils.isEmpty(name))
|
||||
warning = getString(R.string.title_no_name);
|
||||
else if (TextUtils.isEmpty(email))
|
||||
warning = getString(R.string.title_no_email);
|
||||
else if (!Helper.EMAIL_ADDRESS.matcher(email).matches())
|
||||
warning = getString(R.string.title_email_invalid, email);
|
||||
else if (TextUtils.isEmpty(password))
|
||||
warning = getString(R.string.title_no_password);
|
||||
else {
|
||||
ConnectivityManager cm = Helper.getSystemService(getContext(), ConnectivityManager.class);
|
||||
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
|
||||
if (ani == null || !ani.isConnected())
|
||||
warning = getString(R.string.title_no_internet);
|
||||
}
|
||||
|
||||
if (warning != null) {
|
||||
tvArgument.setText(warning);
|
||||
tvArgument.setVisibility(View.VISIBLE);
|
||||
getMainHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
scroll.smoothScrollTo(0, tvArgument.getBottom());
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString("name", etName.getText().toString().trim());
|
||||
args.putString("email", etEmail.getText().toString().trim());
|
||||
args.putString("password", tilPassword.getEditText().getText().toString());
|
||||
args.putString("name", name);
|
||||
args.putString("email", email);
|
||||
args.putString("password", password);
|
||||
args.putBoolean("update", cbUpdate.isChecked());
|
||||
args.putBoolean("check", check);
|
||||
args.putParcelable("best", bestProvider);
|
||||
|
@ -293,6 +350,9 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
tvPatience.setVisibility(check ? View.VISIBLE : View.GONE);
|
||||
pbSave.setVisibility(check ? View.GONE : View.VISIBLE);
|
||||
grpError.setVisibility(View.GONE);
|
||||
grpManual.setVisibility(View.GONE);
|
||||
tvArgument.setVisibility(View.GONE);
|
||||
tvErrorHint.setVisibility(View.GONE);
|
||||
tvInstructions.setVisibility(View.GONE);
|
||||
btnHelp.setVisibility(View.GONE);
|
||||
cbUpdate.setVisibility(check ? View.GONE : View.VISIBLE);
|
||||
|
@ -319,23 +379,9 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
boolean check = args.getBoolean("check");
|
||||
EmailProvider best = args.getParcelable("best");
|
||||
|
||||
if (TextUtils.isEmpty(name))
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_no_name));
|
||||
if (TextUtils.isEmpty(email))
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_no_email));
|
||||
if (!Helper.EMAIL_ADDRESS.matcher(email).matches())
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_email_invalid, email));
|
||||
if (TextUtils.isEmpty(password))
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_no_password));
|
||||
|
||||
int at = email.indexOf('@');
|
||||
String username = email.substring(0, at);
|
||||
|
||||
ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class);
|
||||
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
|
||||
if (ani == null || !ani.isConnected())
|
||||
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
|
||||
|
||||
Throwable fail = null;
|
||||
List<EmailProvider> providers;
|
||||
if (best == null)
|
||||
|
@ -576,13 +622,7 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
}
|
||||
|
||||
// Set swipe left/right folder
|
||||
for (EntityFolder folder : folders)
|
||||
if (EntityFolder.TRASH.equals(folder.type))
|
||||
account.swipe_left = folder.id;
|
||||
else if (EntityFolder.ARCHIVE.equals(folder.type))
|
||||
account.swipe_right = folder.id;
|
||||
|
||||
db.account().updateAccount(account);
|
||||
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
|
||||
|
||||
// Create identity
|
||||
EntityIdentity identity = new EntityIdentity();
|
||||
|
@ -679,8 +719,11 @@ public class FragmentQuickSetup extends FragmentBase {
|
|||
if (provider != null && provider.appPassword)
|
||||
message += "\n\n" + getString(R.string.title_setup_app_password_hint);
|
||||
tvErrorHint.setText(message);
|
||||
tvErrorHint.setVisibility(View.VISIBLE);
|
||||
if (provider == null)
|
||||
grpManual.setVisibility(View.VISIBLE);
|
||||
} else
|
||||
tvErrorHint.setText(R.string.title_setup_no_settings_hint);
|
||||
grpManual.setVisibility(View.VISIBLE);
|
||||
|
||||
if (ex instanceof IllegalArgumentException || ex instanceof UnknownHostException) {
|
||||
tvError.setText(ex.getMessage());
|
||||
|
|
|
@ -43,6 +43,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
|
@ -86,7 +87,7 @@ public class FragmentRule extends FragmentBase {
|
|||
|
||||
private TextView tvFolder;
|
||||
private EditText etName;
|
||||
private EditText etGroup;
|
||||
private AutoCompleteTextView etGroup;
|
||||
private EditText etOrder;
|
||||
private CheckBox cbEnabled;
|
||||
private CheckBox cbDaily;
|
||||
|
@ -185,6 +186,7 @@ public class FragmentRule extends FragmentBase {
|
|||
private Group grpDelete;
|
||||
private Group grpLocalOnly;
|
||||
|
||||
private ArrayAdapter<String> adapterGroup;
|
||||
private ArrayAdapter<String> adapterDay;
|
||||
private ArrayAdapter<Action> adapterAction;
|
||||
private ArrayAdapter<EntityIdentity> adapterIdentity;
|
||||
|
@ -369,6 +371,10 @@ public class FragmentRule extends FragmentBase {
|
|||
grpDelete = view.findViewById(R.id.grpDelete);
|
||||
grpLocalOnly = view.findViewById(R.id.grpLocalOnly);
|
||||
|
||||
adapterGroup = new ArrayAdapter<>(getContext(), R.layout.spinner_item1_dropdown, android.R.id.text1);
|
||||
etGroup.setThreshold(1);
|
||||
etGroup.setAdapter(adapterGroup);
|
||||
|
||||
cbDaily.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
|
@ -839,6 +845,7 @@ public class FragmentRule extends FragmentBase {
|
|||
DB db = DB.getInstance(context);
|
||||
data.account = db.account().getAccount(aid);
|
||||
data.folder = db.folder().getFolder(fid);
|
||||
data.groups = db.rule().getGroups();
|
||||
data.identities = db.identity().getSynchronizingIdentities(aid);
|
||||
data.answers = db.answer().getAnswers(false);
|
||||
|
||||
|
@ -851,6 +858,9 @@ public class FragmentRule extends FragmentBase {
|
|||
data.account == null ? "" : data.account.name,
|
||||
data.folder.getDisplayName(getContext())));
|
||||
|
||||
adapterGroup.clear();
|
||||
adapterGroup.addAll(data.groups);
|
||||
|
||||
adapterIdentity.clear();
|
||||
adapterIdentity.addAll(data.identities);
|
||||
|
||||
|
@ -1692,6 +1702,7 @@ public class FragmentRule extends FragmentBase {
|
|||
private static class RefData {
|
||||
EntityAccount account;
|
||||
EntityFolder folder;
|
||||
List<String> groups;
|
||||
List<EntityIdentity> identities;
|
||||
List<EntityAnswer> answers;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ import android.text.style.TypefaceSpan;
|
|||
import android.text.style.URLSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.util.Base64;
|
||||
import android.util.Pair;
|
||||
import android.util.Patterns;
|
||||
import android.view.View;
|
||||
|
||||
|
@ -114,6 +115,7 @@ import java.text.ParsePosition;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -873,17 +875,8 @@ public class HtmlHelper {
|
|||
break;
|
||||
|
||||
case "font-weight":
|
||||
if (element.parent() != null) {
|
||||
Integer fweight = getFontWeight(value);
|
||||
if (fweight != null && fweight >= 600) {
|
||||
Element strong = new Element("strong");
|
||||
for (Node child : new ArrayList<>(element.childNodes())) {
|
||||
child.remove();
|
||||
strong.appendChild(child);
|
||||
}
|
||||
element.appendChild(strong);
|
||||
}
|
||||
}
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
|
||||
sb.append(key).append(":").append(value).append(";");
|
||||
break;
|
||||
|
||||
case "font-family":
|
||||
|
@ -1889,6 +1882,8 @@ public class HtmlHelper {
|
|||
return 300;
|
||||
case "normal":
|
||||
case "regular":
|
||||
case "unset":
|
||||
case "initial":
|
||||
return 400;
|
||||
case "bolder":
|
||||
case "strong":
|
||||
|
@ -1899,8 +1894,6 @@ public class HtmlHelper {
|
|||
return 900;
|
||||
case "none":
|
||||
case "auto":
|
||||
case "unset":
|
||||
case "initial":
|
||||
case "inherit":
|
||||
return null;
|
||||
}
|
||||
|
@ -3326,6 +3319,11 @@ public class HtmlHelper {
|
|||
Log.i(ex);
|
||||
}
|
||||
break;
|
||||
case "font-weight":
|
||||
Integer fweight = getFontWeight(value);
|
||||
if (fweight != null)
|
||||
setSpan(ssb, new StyleSpan(fweight >= 600 ? Typeface.BOLD : Typeface.NORMAL), start, ssb.length());
|
||||
break;
|
||||
case "font-family":
|
||||
if ("wingdings".equalsIgnoreCase(value)) {
|
||||
if (wingdings == null)
|
||||
|
@ -3773,6 +3771,46 @@ public class HtmlHelper {
|
|||
ssb.setSpan(spans[i], s, e, f);
|
||||
}
|
||||
|
||||
for (Object bold : spans) {
|
||||
if (bold instanceof StyleSpan) {
|
||||
int style = ((StyleSpan) bold).getStyle();
|
||||
if (style == Typeface.BOLD) {
|
||||
int bs = start.get(bold);
|
||||
int be = end.get(bold);
|
||||
|
||||
List<StyleSpan> normal = new ArrayList<>();
|
||||
for (StyleSpan ss : ssb.getSpans(bs, be, StyleSpan.class))
|
||||
if (ss.getStyle() == Typeface.NORMAL)
|
||||
normal.add(ss);
|
||||
|
||||
if (normal.size() > 0) {
|
||||
ssb.removeSpan(bold);
|
||||
|
||||
Collections.sort(normal, new Comparator<StyleSpan>() {
|
||||
@Override
|
||||
public int compare(StyleSpan s1, StyleSpan s2) {
|
||||
int s = Integer.compare(ssb.getSpanStart(s1), ssb.getSpanStart(s2));
|
||||
if (s != 0)
|
||||
return s;
|
||||
return -Integer.compare(ssb.getSpanEnd(s1), ssb.getSpanEnd(s2));
|
||||
}
|
||||
});
|
||||
|
||||
for (StyleSpan n : normal) {
|
||||
int ns = start.get(n);
|
||||
if (ns > bs) {
|
||||
ssb.setSpan(new StyleSpan(Typeface.BOLD), bs, ns, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
bs = end.get(n);
|
||||
}
|
||||
}
|
||||
|
||||
if (bs < be)
|
||||
ssb.setSpan(new StyleSpan(Typeface.BOLD), bs, be, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ssb;
|
||||
}
|
||||
|
||||
|
|
|
@ -3074,6 +3074,12 @@ public class Log {
|
|||
ri.activityInfo.packageName, label, tabs, def));
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String open_with_pkg = prefs.getString("open_with_pkg", null);
|
||||
boolean open_with_tabs = prefs.getBoolean("open_with_tabs", true);
|
||||
size += write(os, String.format("Selected: %s tabs=%b\r\n",
|
||||
open_with_pkg, open_with_tabs));
|
||||
|
||||
size += write(os, "\r\n");
|
||||
} catch (Throwable ex) {
|
||||
size += write(os, String.format("%s\r\n", ex));
|
||||
|
|
|
@ -2307,8 +2307,8 @@ public class MessageHelper {
|
|||
if (records.length == 0)
|
||||
return null;
|
||||
|
||||
Log.i("DKIM got " + records[0].name);
|
||||
Map<String, String> dk = getKeyValues(records[0].name);
|
||||
Log.i("DKIM got " + records[0].response);
|
||||
Map<String, String> dk = getKeyValues(records[0].response);
|
||||
|
||||
String canonic = kv.get("c");
|
||||
Log.i("DKIM canonicalization=" + canonic);
|
||||
|
|
|
@ -77,7 +77,9 @@ public class NoStreamException extends SecurityException {
|
|||
builder.setView(dview);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
|
||||
// https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
||||
builder.setPositiveButton(R.string.title_setup_grant, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
|
|
|
@ -37,17 +37,21 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.mail.MessagingException;
|
||||
|
||||
public class ServiceExternal extends Service {
|
||||
private static final String ACTION_POLL = BuildConfig.APPLICATION_ID + ".POLL";
|
||||
private static final String ACTION_ENABLE = BuildConfig.APPLICATION_ID + ".ENABLE";
|
||||
private static final String ACTION_DISABLE = BuildConfig.APPLICATION_ID + ".DISABLE";
|
||||
private static final String ACTION_INTERVAL = BuildConfig.APPLICATION_ID + ".INTERVAL";
|
||||
private static final String ACTION_RULE = BuildConfig.APPLICATION_ID + ".RULE";
|
||||
private static final String ACTION_DISCONNECT_ME = BuildConfig.APPLICATION_ID + ".DISCONNECT.ME";
|
||||
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.POLL --es account Gmail
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.ENABLE --es account Gmail
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.DISABLE --es account Gmail
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.INTERVAL --ei minutes {0, 15, 30, 60, 120, 240, 480, 1440}
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.RULE --es account Gmail -e rule Test
|
||||
// adb shell am start-foreground-service -a eu.faircode.email.DISCONNECT
|
||||
|
||||
@Override
|
||||
|
@ -96,6 +100,9 @@ public class ServiceExternal extends Service {
|
|||
case ACTION_INTERVAL:
|
||||
interval(context, intent);
|
||||
break;
|
||||
case ACTION_RULE:
|
||||
rule(context, intent);
|
||||
break;
|
||||
case ACTION_DISCONNECT_ME:
|
||||
disconnect(context, intent);
|
||||
break;
|
||||
|
@ -196,6 +203,53 @@ public class ServiceExternal extends Service {
|
|||
ServiceSynchronize.eval(context, "external account=" + accountName + " enabled=" + enabled);
|
||||
}
|
||||
|
||||
private static void rule(Context context, Intent intent) throws IOException, JSONException, MessagingException {
|
||||
String accountName = intent.getStringExtra("account");
|
||||
String ruleName = intent.getStringExtra("rule");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
EntityAccount account = db.account().getAccount(accountName);
|
||||
if (account == null)
|
||||
throw new IllegalArgumentException("Account not found name=" + accountName);
|
||||
|
||||
List<EntityRule> rules = db.rule().getRuleByName(account.id, ruleName);
|
||||
if (rules == null || rules.size() == 0)
|
||||
throw new IllegalArgumentException("Rule not found name=" + ruleName);
|
||||
if (rules.size() != 1)
|
||||
throw new IllegalArgumentException("Rule ambiguous name=" + ruleName);
|
||||
|
||||
EntityRule rule = rules.get(0);
|
||||
List<Long> ids = db.message().getMessageIdsByFolder(rule.folder);
|
||||
if (ids == null || ids.size() == 0)
|
||||
return;
|
||||
|
||||
// Check header conditions
|
||||
for (long mid : ids) {
|
||||
EntityMessage message = db.message().getMessage(mid);
|
||||
if (message == null || message.ui_hide)
|
||||
continue;
|
||||
rule.matches(context, message, null, null);
|
||||
}
|
||||
|
||||
int applied = 0;
|
||||
for (long mid : ids)
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
EntityMessage message = db.message().getMessage(mid);
|
||||
if (message == null || message.ui_hide)
|
||||
continue;
|
||||
|
||||
EntityLog.log(context, "Executing rules message=" + message.id);
|
||||
applied = EntityRule.run(context, rules, message, null, null);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
EntityLog.log(context, "Executing rule=" + rule.name + " applied=" + applied);
|
||||
}
|
||||
|
||||
private static void disconnect(Context context, Intent intent) throws IOException, JSONException {
|
||||
DisconnectBlacklist.download(context);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
|
|||
}
|
||||
|
||||
void init(int height, int maxHeight, float size, Pair<Integer, Integer> position, boolean force_light, IWebView intf) {
|
||||
Log.i("Init height=" + height + "/" + maxHeight + " size=" + size);
|
||||
Log.i("Init height=" + height + "/" + maxHeight + " size=" + size + " accelerated=" + isHardwareAccelerated());
|
||||
|
||||
if (maxHeight == 0) {
|
||||
Log.e("WebView max height zero");
|
||||
|
@ -188,7 +188,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
|
|||
@Override
|
||||
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
|
||||
Log.i("Scroll (x,y)=" + scrollX + "," + scrollY);
|
||||
intf.onScrollChange(scrollX, scrollY);
|
||||
intf.onScrollChange(scrollX - oldScrollX, scrollY - oldScrollY, scrollX, scrollY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -316,18 +316,20 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
|
|||
intercept = (yoff > 0 || dy >= 0) && (yoff < bottom || dy <= 0);
|
||||
}
|
||||
|
||||
int xrange = computeHorizontalScrollRange();
|
||||
int xextend = computeHorizontalScrollExtent();
|
||||
boolean canScrollHorizontal = (xrange > xextend);
|
||||
if (canScrollHorizontal) {
|
||||
int right = xrange - xextend;
|
||||
int xoff = computeHorizontalScrollOffset();
|
||||
int ldx = xoff - lastXoff;
|
||||
float dx = lastX - event.getX();
|
||||
intercept = (xoff > 0 || dx >= 0) &&
|
||||
(xoff < right || dx <= 0) &&
|
||||
(Math.signum(dx) == Math.signum(ldx));
|
||||
lastXoff = xoff;
|
||||
if (!intercept) {
|
||||
int xrange = computeHorizontalScrollRange();
|
||||
int xextend = computeHorizontalScrollExtent();
|
||||
boolean canScrollHorizontal = (xrange > xextend);
|
||||
if (canScrollHorizontal) {
|
||||
int right = xrange - xextend;
|
||||
int xoff = computeHorizontalScrollOffset();
|
||||
int ldx = xoff - lastXoff;
|
||||
float dx = lastX - event.getX();
|
||||
intercept = (xoff > 0 || dx >= 0) &&
|
||||
(xoff < right || dx <= 0) &&
|
||||
(Math.signum(dx) == Math.signum(ldx));
|
||||
lastXoff = xoff;
|
||||
}
|
||||
}
|
||||
}
|
||||
getParent().requestDisallowInterceptTouchEvent(intercept || event.getPointerCount() > 1);
|
||||
|
@ -410,7 +412,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
|
|||
|
||||
void onScaleChanged(float newScale);
|
||||
|
||||
void onScrollChange(int scrollX, int scrollY);
|
||||
void onScrollChange(int dx, int dy, int scrollX, int scrollY);
|
||||
|
||||
boolean onOpenLink(String url);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/etName" />
|
||||
|
||||
<eu.faircode.email.EditTextPlain
|
||||
<eu.faircode.email.EditTextAutoComplete
|
||||
android:id="@+id/etGroup"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -153,6 +153,19 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvPatience" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvArgument"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:autoLink="web"
|
||||
android:text="Argument"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textColor="?attr/colorWarning"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvProgress" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvErrorTitle"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -162,7 +175,7 @@
|
|||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvProgress" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvArgument" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvError"
|
||||
|
@ -189,17 +202,39 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvError" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvManualHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_setup_no_settings_hint"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvErrorHint" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnManual"
|
||||
style="?android:attr/buttonStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableEnd="@drawable/twotone_edit_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/title_setup_manual_setup"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvManualHint" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvErrorRemark"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_setup_quick_support"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvErrorHint" />
|
||||
app:layout_constraintTop_toBottomOf="@id/btnManual" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvInstructions"
|
||||
|
@ -416,6 +451,12 @@
|
|||
android:id="@+id/grpError"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="tvErrorTitle,tvError,tvErrorHint,tvErrorRemark,btnSupport" />
|
||||
app:constraint_referenced_ids="tvErrorTitle,tvError,tvErrorRemark,btnSupport" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/grpManual"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="tvManualHint,btnManual" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</eu.faircode.email.ScrollViewEx>
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/etName" />
|
||||
|
||||
<eu.faircode.email.EditTextPlain
|
||||
<eu.faircode.email.EditTextAutoComplete
|
||||
android:id="@+id/etGroup"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -283,6 +283,7 @@
|
|||
<string name="title_setup_done">Done</string>
|
||||
<string name="title_setup_still">Still to do</string>
|
||||
<string name="title_setup_error">Error</string>
|
||||
<string name="title_setup_manual_setup">Manual setup</string>
|
||||
<string name="title_setup_pop">Use POP3 (not recommended)</string>
|
||||
<string name="title_setup_recent">Use \'recent\' for multiple email clients</string>
|
||||
<string name="title_setup_configuring">Configuring account …</string>
|
||||
|
@ -1866,7 +1867,7 @@
|
|||
<string name="title_rule_daily">Run daily (only)</string>
|
||||
<string name="title_rule_age">Messages older than (days)</string>
|
||||
<string name="title_rule_stop">Stop processing rules after executing this rule</string>
|
||||
<string name="title_rule_stop_remark">If the rule is part of a group, only the processing of the group will be stopped</string>
|
||||
<string name="title_rule_stop_remark">If the rule is part of a group, the processing of all rules of this group will be stopped</string>
|
||||
<string name="title_rule_sender">Sender contains</string>
|
||||
<string name="title_rule_sender_known">Sender is a contact</string>
|
||||
<string name="title_rule_recipient">Recipient contains</string>
|
||||
|
|
|
@ -2050,4 +2050,18 @@
|
|||
port="465"
|
||||
starttls="false" />
|
||||
</provider>
|
||||
<provider
|
||||
name="FREE!"
|
||||
appPassword="true"
|
||||
domain="free\\.de"
|
||||
link="https://faq.free.de/content/310251/44/de/wie-lauten-die-einstellungen-fuer-den-zugriff-auf-mein-free-@freede-mailkonto.html">
|
||||
<imap
|
||||
host="pop.free.de"
|
||||
port="993"
|
||||
starttls="false" />
|
||||
<smtp
|
||||
host="smtp.free.de"
|
||||
port="465"
|
||||
starttls="false" />
|
||||
</provider>
|
||||
</providers>
|
||||
|
|
|
@ -561,6 +561,7 @@
|
|||
<action android:name="${applicationId}.ENABLE" />
|
||||
<action android:name="${applicationId}.DISABLE" />
|
||||
<action android:name="${applicationId}.INTERVAL" />
|
||||
<action android:name="${applicationId}.RULE" />
|
||||
<action android:name="${applicationId}.DISCONNECT.ME" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue