Added translation of received messages

This commit is contained in:
M66B 2021-06-27 14:50:54 +02:00
parent 66fe572bb1
commit c908bca9dc
6 changed files with 237 additions and 19 deletions

View File

@ -304,6 +304,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private static final int MAX_RECIPIENTS_COMPACT = 3;
private static final int MAX_RECIPIENTS_NORMAL = 7;
private static final int MAX_QUOTE_LEVEL = 3;
private static final int MAX_TRANSLATE = 1000; // characters
// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml
private static final List<String> IMAP_KEYWORDS_BLACKLIST = Collections.unmodifiableList(Arrays.asList(
@ -443,6 +444,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private ImageButton ibEvent;
private ImageButton ibSearchText;
private ImageButton ibSearch;
private ImageButton ibTranslate;
private ImageButton ibHide;
private ImageButton ibSeen;
private ImageButton ibAnswer;
@ -675,6 +677,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent = vsBody.findViewById(R.id.ibEvent);
ibSearchText = vsBody.findViewById(R.id.ibSearchText);
ibSearch = vsBody.findViewById(R.id.ibSearch);
ibTranslate = vsBody.findViewById(R.id.ibTranslate);
ibHide = vsBody.findViewById(R.id.ibHide);
ibSeen = vsBody.findViewById(R.id.ibSeen);
ibAnswer = vsBody.findViewById(R.id.ibAnswer);
@ -784,6 +787,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent.setOnClickListener(this);
ibSearchText.setOnClickListener(this);
ibSearch.setOnClickListener(this);
ibTranslate.setOnClickListener(this);
ibTranslate.setOnLongClickListener(this);
ibHide.setOnClickListener(this);
ibSeen.setOnClickListener(this);
ibAnswer.setOnClickListener(this);
@ -905,6 +910,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent.setOnClickListener(null);
ibSearchText.setOnClickListener(null);
ibSearch.setOnClickListener(null);
ibTranslate.setOnClickListener(null);
ibTranslate.setOnLongClickListener(null);
ibHide.setOnClickListener(null);
ibSeen.setOnClickListener(null);
ibAnswer.setOnClickListener(null);
@ -1382,6 +1389,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent.setVisibility(View.GONE);
ibSearchText.setVisibility(View.GONE);
ibSearch.setVisibility(View.GONE);
ibTranslate.setVisibility(View.GONE);
ibHide.setVisibility(View.GONE);
ibSeen.setVisibility(View.GONE);
ibAnswer.setVisibility(View.GONE);
@ -1591,6 +1599,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent.setVisibility(View.GONE);
ibSearchText.setVisibility(View.GONE);
ibSearch.setVisibility(View.GONE);
ibTranslate.setVisibility(View.GONE);
ibHide.setVisibility(View.GONE);
ibSeen.setVisibility(View.GONE);
ibAnswer.setVisibility(View.GONE);
@ -1774,6 +1783,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
boolean button_notes = prefs.getBoolean("button_notes", false);
boolean button_seen = prefs.getBoolean("button_seen", false);
boolean button_hide = prefs.getBoolean("button_hide", false);
boolean button_translate = prefs.getBoolean("button_translate", false);
boolean button_search = prefs.getBoolean("button_search", false);
boolean button_search_text = prefs.getBoolean("button_search_text", false);
boolean button_event = prefs.getBoolean("button_event", false);
@ -1801,6 +1811,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibEvent.setVisibility(tools && button_event && message.content ? View.VISIBLE : View.GONE);
ibSearchText.setVisibility(tools && button_search_text && message.content && !full ? View.VISIBLE : View.GONE);
ibSearch.setVisibility(tools && button_search && (froms > 0 || tos > 0) && !outbox ? View.VISIBLE : View.GONE);
ibTranslate.setVisibility(tools && button_translate && DeepL.isAvailable(context) && message.content ? View.VISIBLE : View.GONE);
ibHide.setVisibility(tools && button_hide && !outbox ? View.VISIBLE : View.GONE);
ibSeen.setVisibility(tools && button_seen && !outbox && seen ? View.VISIBLE : View.GONE);
ibAnswer.setVisibility(!tools || outbox || (!expand_all && expand_one) ? View.GONE : View.VISIBLE);
@ -3187,6 +3198,13 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
onSearchText(message);
} else if (id == R.id.ibSearch) {
onSearchContact(message);
} else if (id == R.id.ibTranslate) {
if (DeepL.canTranslate(context))
onActionTranslate(message);
else {
DeepL.FragmentDialogDeepL fragment = new DeepL.FragmentDialogDeepL();
fragment.show(parentFragment.getParentFragmentManager(), "deepl:configure");
}
} else if (id == R.id.ibAnswer) {
onActionAnswer(message, ibAnswer);
} else if (id == R.id.ibNotes) {
@ -3344,6 +3362,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
} else if (id == R.id.ibNotes) {
onActionCopyNote(message);
return true;
} else if (id == R.id.ibTranslate) {
DeepL.FragmentDialogDeepL fragment = new DeepL.FragmentDialogDeepL();
fragment.show(parentFragment.getParentFragmentManager(), "deepl:configure");
return true;
} else if (id == R.id.ibFull) {
onActionOpenFull(message);
return true;
@ -4831,6 +4853,15 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
fragment.show(parentFragment.getParentFragmentManager(), "edit:notes");
}
private void onActionTranslate(TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
FragmentDialogTranslate fragment = new FragmentDialogTranslate();
fragment.setArguments(args);
fragment.show(parentFragment.getParentFragmentManager(), "message:translate");
}
private void onSearchText(TupleMessageEx message) {
LayoutInflater inflater = LayoutInflater.from(context);
View dview = inflater.inflate(R.layout.popup_search_in_text, null, false);
@ -6618,6 +6649,64 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
}
public static class FragmentDialogTranslate extends FragmentDialogBase {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final Context context = getContext();
final View view = LayoutInflater.from(context).inflate(R.layout.dialog_translate, null);
final TextView tvTranslated = view.findViewById(R.id.tvTranslated);
final TextView tvLanguage = view.findViewById(R.id.tvLanguage);
final ContentLoadingProgressBar pbWait = view.findViewById(R.id.pbWait);
tvTranslated.setText(null);
tvLanguage.setText(null);
new SimpleTask<DeepL.Translation>() {
@Override
protected void onPreExecute(Bundle args) {
pbWait.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Bundle args) {
pbWait.setVisibility(View.GONE);
}
@Override
protected DeepL.Translation onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
File file = EntityMessage.getFile(context, id);
Document d = JsoupEx.parse(file);
HtmlHelper.truncate(d, MAX_TRANSLATE);
String text = d.text();
String language = DeepL.getCurrentLanguage(context);
return DeepL.translate(text, language, context);
}
@Override
protected void onExecuted(Bundle args, DeepL.Translation translation) {
tvTranslated.setText(translation.translated_text);
tvLanguage.setText(getString(R.string.title_from_to,
translation.detected_language, translation.target_language));
}
@Override
protected void onException(Bundle args, Throwable ex) {
tvTranslated.setText(ex.toString());
}
}.execute(this, getArguments(), "message:translate");
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(android.R.string.ok, null);
return builder.create();
}
}
public static class FragmentDialogKeywordManage extends FragmentDialogBase {
@NonNull
@Override
@ -6931,6 +7020,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
final CheckBox cbHide = dview.findViewById(R.id.cbHide);
final CheckBox cbSearch = dview.findViewById(R.id.cbSearch);
final CheckBox cbSearchText = dview.findViewById(R.id.cbSearchText);
final CheckBox cbTranslate = dview.findViewById(R.id.cbTranslate);
final CheckBox cbEvent = dview.findViewById(R.id.cbEvent);
final CheckBox cbShare = dview.findViewById(R.id.cbShare);
final CheckBox cbPin = dview.findViewById(R.id.cbPin);
@ -6939,6 +7029,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
final CheckBox cbUnsubscribe = dview.findViewById(R.id.cbUnsubscribe);
final CheckBox cbRule = dview.findViewById(R.id.cbRule);
cbTranslate.setVisibility(DeepL.isAvailable(context) &&
DeepL.getCurrentLanguage(context) != null
? View.VISIBLE : View.GONE);
cbPin.setVisibility(Shortcuts.can(context) ? View.VISIBLE : View.GONE);
cbJunk.setChecked(prefs.getBoolean("button_junk", true));
@ -6952,6 +7045,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
cbHide.setChecked(prefs.getBoolean("button_hide", false));
cbSearch.setChecked(prefs.getBoolean("button_search", false));
cbSearchText.setChecked(prefs.getBoolean("button_search_text", false));
cbTranslate.setChecked(prefs.getBoolean("button_translate", false));
cbEvent.setChecked(prefs.getBoolean("button_event", false));
cbShare.setChecked(prefs.getBoolean("button_share", false));
cbPin.setChecked(prefs.getBoolean("button_pin", false));
@ -6977,6 +7071,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
editor.putBoolean("button_hide", cbHide.isChecked());
editor.putBoolean("button_search", cbSearch.isChecked());
editor.putBoolean("button_search_text", cbSearchText.isChecked());
editor.putBoolean("button_translate", cbTranslate.isChecked());
editor.putBoolean("button_event", cbEvent.isChecked());
editor.putBoolean("button_share", cbShare.isChecked());
editor.putBoolean("button_pin", cbPin.isChecked());

View File

@ -62,10 +62,12 @@ import javax.net.ssl.HttpsURLConnection;
public class DeepL {
// https://www.deepl.com/docs-api/
private static JSONArray jlanguages = null;
private static final int DEEPL_TIMEOUT = 20; // seconds
// curl https://api-free.deepl.com/v2/languages \
// -d auth_key=42c191db-21ba-9b96-2464-47a9a5e81b4a:fx \
// -d auth_key=... \
// -d type=target
public static boolean isAvailable(Context context) {
@ -81,17 +83,16 @@ public class DeepL {
}
public static List<Language> getTargetLanguages(Context context) {
try (InputStream is = context.getAssets().open("deepl.json")) {
String json = Helper.readStream(is);
JSONArray jarray = new JSONArray(json);
try {
ensureLanguages(context);
String pkg = context.getPackageName();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<Language> languages = new ArrayList<>();
Map<String, Integer> frequencies = new HashMap<>();
for (int i = 0; i < jarray.length(); i++) {
JSONObject jlanguage = jarray.getJSONObject(i);
for (int i = 0; i < jlanguages.length(); i++) {
JSONObject jlanguage = jlanguages.getJSONObject(i);
String name = jlanguage.getString("name");
String target = jlanguage.getString("language");
@ -132,6 +133,36 @@ public class DeepL {
}
}
public static String getCurrentLanguage(Context context) {
try {
ensureLanguages(context);
Locale locale = Locale.getDefault();
for (int i = 0; i < jlanguages.length(); i++) {
JSONObject jlanguage = jlanguages.getJSONObject(i);
String language = jlanguage.getString("language");
if (language.equalsIgnoreCase(locale.toLanguageTag()) ||
language.equalsIgnoreCase(locale.getLanguage())) {
return language;
}
}
} catch (Throwable ex) {
Log.e(ex);
}
return null;
}
private static void ensureLanguages(Context context) throws IOException, JSONException {
if (jlanguages != null)
return;
try (InputStream is = context.getAssets().open("deepl.json")) {
String json = Helper.readStream(is);
jlanguages = new JSONArray(json);
}
}
public static Pair<Integer, Integer> getParagraph(EditText etBody) {
int start = etBody.getSelectionStart();
int end = etBody.getSelectionEnd();
@ -171,7 +202,7 @@ public class DeepL {
return null;
}
public static String translate(String text, String target, Context context) throws IOException, JSONException {
public static Translation translate(String text, String target, Context context) throws IOException, JSONException {
String request =
"text=" + URLEncoder.encode(text, StandardCharsets.UTF_8.name()) +
"&target_lang=" + URLEncoder.encode(target, StandardCharsets.UTF_8.name());
@ -212,9 +243,12 @@ public class DeepL {
if (jtranslations.length() == 0)
throw new FileNotFoundException();
JSONObject jtranslation = (JSONObject) jtranslations.get(0);
String detected = jtranslation.getString("detected_source_language");
String translated = jtranslation.getString("text");
return translated;
Translation result = new Translation();
result.target_language = target;
result.detected_language = jtranslation.getString("detected_source_language");
result.translated_text = jtranslation.getString("text");
return result;
} finally {
connection.disconnect();
}
@ -273,6 +307,12 @@ public class DeepL {
}
}
public static class Translation {
public String detected_language;
public String target_language;
public String translated_text;
}
public static class FragmentDialogDeepL extends FragmentDialogBase {
@NonNull
@Override
@ -324,8 +364,8 @@ public class DeepL {
@Override
protected void onException(Bundle args, Throwable ex) {
if (BuildConfig.DEBUG)
Log.unexpectedError(getParentFragmentManager(), ex);
tvUsage.setText(Log.formatThrowable(ex, false));
tvUsage.setVisibility(View.VISIBLE);
}
}.execute(this, new Bundle(), "deepl:usage");
}

View File

@ -755,21 +755,21 @@ public class FragmentCompose extends FragmentBase {
args.putString("target", target);
args.putString("text", text);
new SimpleTask<String>() {
new SimpleTask<DeepL.Translation>() {
@Override
protected void onPreExecute(Bundle args) {
ToastEx.makeText(getContext(), R.string.title_translating, Toast.LENGTH_SHORT).show();
}
@Override
protected String onExecute(Context context, Bundle args) throws Throwable {
protected DeepL.Translation onExecute(Context context, Bundle args) throws Throwable {
String target = args.getString("target");
String text = args.getString("text");
return DeepL.translate(text, target, context);
}
@Override
protected void onExecuted(Bundle args, String translated) {
protected void onExecuted(Bundle args, DeepL.Translation translation) {
if (paragraph.second > edit.length())
return;
@ -781,8 +781,8 @@ public class FragmentCompose extends FragmentBase {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
// Insert translated text
edit.insert(paragraph.second, "\n\n" + translated);
etBody.setSelection(paragraph.second + 2 + translated.length());
edit.insert(paragraph.second, "\n\n" + translation.translated_text);
etBody.setSelection(paragraph.second + 2 + translation.translated_text.length());
boolean small = prefs.getBoolean("deepl_small", false);
if (small) {

View File

@ -174,6 +174,19 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSearch" />
<CheckBox
android:id="@+id/cbTranslate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:drawableEnd="@drawable/outline_translate_24"
android:drawablePadding="6dp"
android:text="@string/title_translate"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSearchText" />
<CheckBox
android:id="@+id/cbEvent"
android:layout_width="0dp"
@ -185,7 +198,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSearchText" />
app:layout_constraintTop_toBottomOf="@id/cbTranslate" />
<CheckBox
android:id="@+id/cbShare"

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.faircode.email.ScrollViewEx xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
android:scrollbarStyle="outsideOverlay">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<eu.faircode.email.FixedTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/etKeyword"
android:text="@string/title_translate"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvTranslated"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="60dp"
android:text="Text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvLanguage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_from_to"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTranslated" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/tvTranslated"
app:layout_constraintEnd_toEndOf="@id/tvTranslated"
app:layout_constraintStart_toStartOf="@id/tvTranslated"
app:layout_constraintTop_toTopOf="@id/tvTranslated" />
</androidx.constraintlayout.widget.ConstraintLayout>
</eu.faircode.email.ScrollViewEx>

View File

@ -42,7 +42,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="3dp"
app:constraint_referenced_ids="ibMore,ibInbox,ibJunk,ibTrash,ibArchive,ibMove,ibCopy,ibKeywords,ibLabels,ibNotes,ibAnswer,ibSeen,ibHide,ibSearch,ibSearchText,ibEvent,ibShare,ibPin,ibPrint,ibHeaders,ibUnsubscribe,ibRule,ibUndo"
app:constraint_referenced_ids="ibMore,ibInbox,ibJunk,ibTrash,ibArchive,ibMove,ibCopy,ibKeywords,ibLabels,ibNotes,ibAnswer,ibSeen,ibHide,ibTranslate,ibSearch,ibSearchText,ibEvent,ibShare,ibPin,ibPrint,ibHeaders,ibUnsubscribe,ibRule,ibUndo"
app:flow_horizontalBias="0"
app:flow_horizontalGap="3dp"
app:flow_horizontalStyle="packed"
@ -201,6 +201,18 @@
app:srcCompat="@drawable/twotone_visibility_off_24"
tools:ignore="MissingConstraints" />
<ImageButton
android:id="@+id/ibTranslate"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/title_translate"
android:padding="6dp"
android:scaleType="fitCenter"
android:tooltipText="@string/title_translate"
app:srcCompat="@drawable/outline_translate_24"
tools:ignore="MissingConstraints" />
<ImageButton
android:id="@+id/ibSearch"
android:layout_width="36dp"