diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 023dcdfc1f..2cb94117c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,6 +91,8 @@ + + { case EntityRule.TYPE_ANSWER: tvAction.setText(R.string.title_rule_answer); break; + case EntityRule.TYPE_TTS: + tvAction.setText(R.string.title_rule_tts); + break; case EntityRule.TYPE_AUTOMATION: tvAction.setText(R.string.title_rule_automation); break; diff --git a/app/src/main/java/eu/faircode/email/EntityRule.java b/app/src/main/java/eu/faircode/email/EntityRule.java index e19e86b625..990452a3a8 100644 --- a/app/src/main/java/eu/faircode/email/EntityRule.java +++ b/app/src/main/java/eu/faircode/email/EntityRule.java @@ -98,6 +98,7 @@ public class EntityRule { static final int TYPE_KEYWORD = 11; static final int TYPE_HIDE = 12; static final int TYPE_IMPORTANCE = 13; + static final int TYPE_TTS = 14; static final String ACTION_AUTOMATION = BuildConfig.APPLICATION_ID + ".AUTOMATION"; static final String EXTRA_RULE = "rule"; @@ -323,6 +324,8 @@ public class EntityRule { return onActionCopy(context, message, jaction); case TYPE_ANSWER: return onActionAnswer(context, message, jaction); + case TYPE_TTS: + return onActionTts(context, message, jaction); case TYPE_AUTOMATION: return onActionAutomation(context, message, jaction); default: @@ -480,6 +483,24 @@ public class EntityRule { return true; } + private boolean onActionTts(Context context, EntityMessage message, JSONObject jargs) { + StringBuilder sb = new StringBuilder(); + sb.append(context.getString(R.string.title_rule_tts_prefix)); + + if (message.from != null && message.from.length > 0) + sb.append(' ').append(context.getString(R.string.title_from)) + .append(' ').append(MessageHelper.formatAddressesShort(message.from)); + + if (!TextUtils.isEmpty(message.subject)) + sb.append(' ').append(context.getString(R.string.title_subject)) + .append(' ').append(message.subject); + + EntityLog.log(context, "TTS queued language=" + message.language + " text=" + sb.toString()); + TTSHelper.speak(context, "rule:" + message.id, sb.toString(), message.language); + + return true; + } + private boolean onActionSnooze(Context context, EntityMessage message, JSONObject jargs) throws JSONException { int duration = jargs.getInt("duration"); boolean schedule_end = jargs.optBoolean("schedule_end", false); diff --git a/app/src/main/java/eu/faircode/email/FragmentRule.java b/app/src/main/java/eu/faircode/email/FragmentRule.java index b5213f9c33..a5f79950a4 100644 --- a/app/src/main/java/eu/faircode/email/FragmentRule.java +++ b/app/src/main/java/eu/faircode/email/FragmentRule.java @@ -31,6 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.ContactsContract; +import android.speech.tts.TextToSpeech; import android.text.TextUtils; import android.text.format.DateFormat; import android.view.LayoutInflater; @@ -133,6 +134,8 @@ public class FragmentRule extends FragmentBase { private Spinner spAnswer; private CheckBox cbCc; + private Button btnTts; + private TextView tvAutomation; private BottomNavigationView bottom_navigation; @@ -146,6 +149,7 @@ public class FragmentRule extends FragmentBase { private Group grpMove; private Group grpMoveProp; private Group grpAnswer; + private Group grpTts; private Group grpAutomation; private ArrayAdapter adapterDay; @@ -168,6 +172,7 @@ public class FragmentRule extends FragmentBase { private final static int REQUEST_DELETE = 4; private final static int REQUEST_SCHEDULE_START = 5; private final static int REQUEST_SCHEDULE_END = 6; + private final static int REQUEST_TTS = 7; @Override public void onCreate(Bundle savedInstanceState) { @@ -247,6 +252,7 @@ public class FragmentRule extends FragmentBase { spAnswer = view.findViewById(R.id.spAnswer); cbCc = view.findViewById(R.id.cbCc); + btnTts = view.findViewById(R.id.btnTts); tvAutomation = view.findViewById(R.id.tvAutomation); bottom_navigation = view.findViewById(R.id.bottom_navigation); @@ -261,6 +267,7 @@ public class FragmentRule extends FragmentBase { grpMove = view.findViewById(R.id.grpMove); grpMoveProp = view.findViewById(R.id.grpMoveProp); grpAnswer = view.findViewById(R.id.grpAnswer); + grpTts = view.findViewById(R.id.grpTts); grpAutomation = view.findViewById(R.id.grpAutomation); ibSender.setOnClickListener(new View.OnClickListener() { @@ -391,6 +398,7 @@ public class FragmentRule extends FragmentBase { actions.add(new Action(EntityRule.TYPE_COPY, getString(R.string.title_rule_copy))); } actions.add(new Action(EntityRule.TYPE_ANSWER, getString(R.string.title_rule_answer))); + actions.add(new Action(EntityRule.TYPE_TTS, getString(R.string.title_rule_tts))); actions.add(new Action(EntityRule.TYPE_AUTOMATION, getString(R.string.title_rule_automation))); adapterAction.addAll(actions); @@ -454,6 +462,15 @@ public class FragmentRule extends FragmentBase { spIdent.setOnItemSelectedListener(onItemSelectedListener); spAnswer.setOnItemSelectedListener(onItemSelectedListener); + btnTts.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent tts = new Intent(); + tts.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); + startActivityForResult(tts, REQUEST_TTS); + } + }); + tvAutomation.setText(getString(R.string.title_rule_automation_hint, EntityRule.ACTION_AUTOMATION, TextUtils.join(",", new String[]{ @@ -491,6 +508,7 @@ public class FragmentRule extends FragmentBase { grpMove.setVisibility(View.GONE); grpMoveProp.setVisibility(View.GONE); grpAnswer.setVisibility(View.GONE); + grpTts.setVisibility(View.GONE); grpAutomation.setVisibility(View.GONE); pbWait.setVisibility(View.VISIBLE); @@ -634,6 +652,15 @@ public class FragmentRule extends FragmentBase { if (resultCode == RESULT_OK) onScheduleEnd(data.getBundleExtra("args")); break; + case REQUEST_TTS: + if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) + ToastEx.makeText(getContext(), android.R.string.ok, Toast.LENGTH_LONG).show(); + else { + Intent tts = new Intent(); + tts.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); + startActivity(tts); + } + break; } } catch (Throwable ex) { Log.e(ex); @@ -881,6 +908,7 @@ public class FragmentRule extends FragmentBase { grpMove.setVisibility(type == EntityRule.TYPE_MOVE || type == EntityRule.TYPE_COPY ? View.VISIBLE : View.GONE); grpMoveProp.setVisibility(type == EntityRule.TYPE_MOVE ? View.VISIBLE : View.GONE); grpAnswer.setVisibility(type == EntityRule.TYPE_ANSWER ? View.VISIBLE : View.GONE); + grpTts.setVisibility(type == EntityRule.TYPE_TTS ? View.VISIBLE : View.GONE); grpAutomation.setVisibility(type == EntityRule.TYPE_AUTOMATION ? View.VISIBLE : View.GONE); } diff --git a/app/src/main/java/eu/faircode/email/TTSHelper.java b/app/src/main/java/eu/faircode/email/TTSHelper.java new file mode 100644 index 0000000000..5ebf1e05a6 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/TTSHelper.java @@ -0,0 +1,44 @@ +package eu.faircode.email; + +import android.content.Context; +import android.speech.tts.TextToSpeech; + +import java.util.Locale; + +public class TTSHelper { + private static boolean initialized; + private static TextToSpeech instance; + + static void speak(Context context, final String utteranceId, final String text, final String language) { + // https://developer.android.com/reference/android/speech/tts/TextToSpeech + // https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html + + final Runnable speak = new Runnable() { + @Override + public void run() { + if (language != null) { + Locale loc = new Locale(language); + if (instance.setLanguage(loc) < 0) + EntityLog.log(context, "TTS unavailable language=" + loc); + } + + instance.speak(text, TextToSpeech.QUEUE_ADD, null, utteranceId); + } + }; + + if (initialized) { + speak.run(); + return; + } + + instance = new TextToSpeech(context, new TextToSpeech.OnInitListener() { + @Override + public void onInit(int status) { + initialized = (status == TextToSpeech.SUCCESS); + Log.i("TTS status=" + status + " ok=" + initialized); + if (initialized) + speak.run(); + } + }); + } +} diff --git a/app/src/main/res/layout/fragment_rule.xml b/app/src/main/res/layout/fragment_rule.xml index f7729b2f6c..6774c85c56 100644 --- a/app/src/main/res/layout/fragment_rule.xml +++ b/app/src/main/res/layout/fragment_rule.xml @@ -689,6 +689,16 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/cbCc" /> +