diff --git a/PRIVACY.md b/PRIVACY.md index a8bddcbba2..5b70216c92 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -31,7 +31,7 @@ FairEmail **can use** these services if they are explicitly enabled (off by defa * [Spamcop](https://www.spamcop.net/) – [Privacy policy](https://www.spamcop.net/fom-serve/cache/168.html) * [Barracuda](https://www.barracudacentral.org/rbl/how-to-use) – [Privacy policy](https://www.barracuda.com/company/legal/trust-center/data-privacy/privacy-policy) * [Thunderbird autoconfiguration](https://developer.mozilla.org/docs/Mozilla/Thunderbird/Autoconfiguration) – [Privacy policy](https://www.mozilla.org/privacy/) - +* [LanguageTool](https://languagetool.org/) – [Privacy policy](https://languagetool.org/legal/privacy) FairEmail **can access** the websites at the domain names of email addresses if [Brand Indicators for Message Identification](https://en.wikipedia.org/wiki/Brand_Indicators_for_Message_Identification) (BIMI) diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 831c2dd66c..0e9767a71f 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -81,6 +81,7 @@ import android.text.style.ImageSpan; import android.text.style.ParagraphStyle; import android.text.style.QuoteSpan; import android.text.style.RelativeSizeSpan; +import android.text.style.SuggestionSpan; import android.text.style.URLSpan; import android.util.LogPrinter; import android.util.Pair; @@ -1814,6 +1815,15 @@ public class FragmentCompose extends FragmentBase { for (int i = 0; i < m.size(); i++) bottom_navigation.findViewById(m.getItem(i).getItemId()).setOnLongClickListener(null); + if (!BuildConfig.PLAY_STORE_RELEASE) + bottom_navigation.findViewById(R.id.action_save).setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + onLanguageTool(); + return true; + } + }); + bottom_navigation.findViewById(R.id.action_send).setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { @@ -2379,6 +2389,53 @@ public class FragmentCompose extends FragmentBase { popupMenu.showWithIcons(context, anchor); } + private void onLanguageTool() { + etBody.clearComposingText(); + + Bundle args = new Bundle(); + args.putCharSequence("text", etBody.getText()); + + new SimpleTask>() { + @Override + protected void onPreExecute(Bundle args) { + setBusy(true); + } + + @Override + protected void onPostExecute(Bundle args) { + setBusy(false); + } + + @Override + protected List onExecute(Context context, Bundle args) throws Throwable { + CharSequence text = args.getCharSequence("text").toString(); + return LanguageTool.getSuggestions(context, text); + } + + @Override + protected void onExecuted(Bundle args, List suggestions) { + // https://developer.android.com/reference/android/text/style/SuggestionSpan + final Context context = getContext(); + Editable edit = etBody.getText(); + + for (LanguageTool.Suggestion suggestion : suggestions) { + Log.i("LT suggestion=" + suggestion); + SuggestionSpan span = new SuggestionSpan(context, + suggestion.replacements.toArray(new String[0]), + SuggestionSpan.FLAG_MISSPELLED); + int start = suggestion.offset; + int end = start + suggestion.length; + edit.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + } + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, args, "compose:lt"); + } + private boolean onActionStyle(int action, View anchor) { Log.i("Style action=" + action); return StyleHelper.apply(action, getViewLifecycleOwner(), anchor, etBody); diff --git a/app/src/main/java/eu/faircode/email/LanguageTool.java b/app/src/main/java/eu/faircode/email/LanguageTool.java new file mode 100644 index 0000000000..1f6f6a189f --- /dev/null +++ b/app/src/main/java/eu/faircode/email/LanguageTool.java @@ -0,0 +1,122 @@ +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 . + + Copyright 2018-2022 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; + +public class LanguageTool { + private static final int LT_TIMEOUT = 20; // seconds + private static final String LT_URI = "https://api.languagetool.org/v2/"; + + static List getSuggestions(Context context, CharSequence text) throws IOException, JSONException { + // https://languagetool.org/http-api/swagger-ui/#!/default/post_check + String request = + "text=" + URLEncoder.encode(text.toString(), StandardCharsets.UTF_8.name()) + + "&language=auto"; + + URL url = new URL(LT_URI + "check"); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setReadTimeout(LT_TIMEOUT * 1000); + connection.setConnectTimeout(LT_TIMEOUT * 1000); + connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Content-Length", Integer.toString(request.length())); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.connect(); + + try { + connection.getOutputStream().write(request.getBytes()); + + int status = connection.getResponseCode(); + if (status != HttpsURLConnection.HTTP_OK) { + String error = "Error " + status + ": " + connection.getResponseMessage(); + try { + InputStream is = connection.getErrorStream(); + if (is != null) + error += "\n" + Helper.readStream(is); + } catch (Throwable ex) { + Log.w(ex); + } + throw new FileNotFoundException(error); + } + + String response = Helper.readStream(connection.getInputStream()); + + List result = new ArrayList<>(); + + JSONObject jroot = new JSONObject(response); + JSONArray jmatches = jroot.getJSONArray("matches"); + for (int i = 0; i < jmatches.length(); i++) { + JSONObject jmatch = jmatches.getJSONObject(i); + + Suggestion suggestion = new Suggestion(); + suggestion.title = jmatch.getString("shortMessage"); + suggestion.description = jmatch.getString("message"); + suggestion.offset = jmatch.getInt("offset"); + suggestion.length = jmatch.getInt("length"); + + JSONArray jreplacements = jmatch.getJSONArray("replacements"); + + suggestion.replacements = new ArrayList<>(); + for (int j = 0; j < jreplacements.length(); j++) { + JSONObject jreplacement = jreplacements.getJSONObject(j); + suggestion.replacements.add(jreplacement.getString("value")); + } + + if (suggestion.replacements.size() > 0) + result.add(suggestion); + } + + return result; + } finally { + connection.disconnect(); + } + } + + static class Suggestion { + String title; // shortMessage + String description; // message + int offset; + int length; + List replacements; + + @Override + public String toString() { + return title; + } + } +}