diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bd88844f..c1d0876d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### [Draconyx](https://en.wikipedia.org/wiki/Draconyx) +### Next version + +* Added rule action to play (alarm) sound +* Small improvements and minor bug fixes +* Updated translations + ### 1.1802 - 2022-01-03 * Improved original message view scrolling diff --git a/FAQ.md b/FAQ.md index 0e1b5ad33e..1daaa49de9 100644 --- a/FAQ.md +++ b/FAQ.md @@ -294,7 +294,7 @@ Fonts, sizes, colors, etc should be material design whenever possible. * [(142) How can I store sent messages in the inbox?](#user-content-faq142) * [~~(143) Can you add a trash folder for POP3 accounts?~~](#user-content-faq143) * [(144) How can I record voice notes?](#user-content-faq144) -* [(145) How can I set a notification sound for an account, folder or sender?](#user-content-faq145) +* [(145) How can I set a notification sound for an account, folder, sender or condition?](#user-content-faq145) * [(146) How can I fix incorrect message times?](#user-content-faq146) * [(147) What should I know about third party versions?](#user-content-faq147) * [(148) How can I use an Apple iCloud account?](#user-content-faq148) @@ -3799,7 +3799,7 @@ Voice notes will automatically be attached.
-**(145) How can I set a notification sound for an account, folder or sender?** +**(145) How can I set a notification sound for an account, folder, sender or condition?** 🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https://github.com/M66B/FairEmail/blob/master/FAQ.md%23user-content-faq145) @@ -3819,7 +3819,14 @@ Sender: * Expand the addresses section by tapping on the down arrow * Tap on the bell icon to create or edit a notification channel and to change the notification sound -The order of precendence is: sender sound, folder sound, account sound and default sound. +Conditional: (since version 1.1803) + +* Long press the folder (inbox) in the folder list and select *Edit rules* +* Add a rule with the big 'plus' button at the bottom right +* Configure a rule condition, select *Play sound* as rule action and select a sound +* For more information about filter rules, please [see here](#user-content-faq71) + +The order of precendence is: conditional sound, sender sound, folder sound, account sound and (default) notification sound. Setting a notification sound for an account, folder or sender requires Android 8 Oreo or later and is a pro feature. diff --git a/app/src/main/assets/CHANGELOG.md b/app/src/main/assets/CHANGELOG.md index f0bd88844f..c1d0876d47 100644 --- a/app/src/main/assets/CHANGELOG.md +++ b/app/src/main/assets/CHANGELOG.md @@ -4,6 +4,12 @@ ### [Draconyx](https://en.wikipedia.org/wiki/Draconyx) +### Next version + +* Added rule action to play (alarm) sound +* Small improvements and minor bug fixes +* Updated translations + ### 1.1802 - 2022-01-03 * Improved original message view scrolling diff --git a/app/src/main/java/eu/faircode/email/AdapterRule.java b/app/src/main/java/eu/faircode/email/AdapterRule.java index e0007d4934..9b12a0573f 100644 --- a/app/src/main/java/eu/faircode/email/AdapterRule.java +++ b/app/src/main/java/eu/faircode/email/AdapterRule.java @@ -186,6 +186,9 @@ public class AdapterRule extends RecyclerView.Adapter { case EntityRule.TYPE_DELETE: tvAction.setText(R.string.title_rule_delete); break; + case EntityRule.TYPE_SOUND: + tvAction.setText(R.string.title_rule_sound); + break; default: throw new IllegalArgumentException("Unknown action type=" + type); } diff --git a/app/src/main/java/eu/faircode/email/EntityRule.java b/app/src/main/java/eu/faircode/email/EntityRule.java index 2d8e159557..e9a97bc4f8 100644 --- a/app/src/main/java/eu/faircode/email/EntityRule.java +++ b/app/src/main/java/eu/faircode/email/EntityRule.java @@ -27,7 +27,6 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.net.Uri; -import android.telephony.TelephonyManager; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -114,6 +113,7 @@ public class EntityRule { static final int TYPE_IMPORTANCE = 13; static final int TYPE_TTS = 14; static final int TYPE_DELETE = 15; + static final int TYPE_SOUND = 16; static final String ACTION_AUTOMATION = BuildConfig.APPLICATION_ID + ".AUTOMATION"; static final String EXTRA_RULE = "rule"; @@ -467,6 +467,8 @@ public class EntityRule { return onActionAutomation(context, message, jaction); case TYPE_DELETE: return onActionDelete(context, message, jaction); + case TYPE_SOUND: + return onActionSound(context, message, jaction); default: throw new IllegalArgumentException("Unknown rule type=" + type + " name=" + name); } @@ -533,6 +535,11 @@ public class EntityRule { return; case TYPE_DELETE: return; + case TYPE_SOUND: + String uri = jargs.optString("uri"); + if (TextUtils.isEmpty(uri)) + throw new IllegalArgumentException(context.getString(R.string.title_rule_select_sound)); + return; default: throw new IllegalArgumentException("Unknown rule type=" + type); } @@ -830,13 +837,8 @@ public class EntityRule { private static void speak(Context context, EntityRule rule, EntityMessage message) throws IOException { Log.i("Speaking name=" + rule.name); - TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - int callState = tm.getCallState(); - if (callState != TelephonyManager.CALL_STATE_IDLE) { - EntityLog.log(context, EntityLog.Type.Rules, message, - "Call state=" + callState + " rule=" + rule.name); + if (message.ui_seen) return; - } Locale locale = (message.language == null ? Locale.getDefault() : new Locale(message.language)); @@ -946,6 +948,34 @@ public class EntityRule { return true; } + private boolean onActionSound(Context context, EntityMessage message, JSONObject jargs) throws JSONException { + Log.i("Speaking name=" + name); + + if (message.ui_seen) + return false; + + Uri uri = Uri.parse(jargs.getString("uri")); + boolean alarm = jargs.getBoolean("alarm"); + + DB db = DB.getInstance(context); + + message.ui_silent = true; + db.message().setMessageUiSilent(message.id, message.ui_silent); + + executor.submit(new Runnable() { + @Override + public void run() { + try { + MediaPlayerHelper.play(context, uri, alarm); + } catch (Throwable ex) { + Log.e(ex); + } + } + }); + + return true; + } + private static Calendar getRelativeCalendar(int minutes, long reference) { int d = minutes / (24 * 60); int h = minutes / 60 % 24; diff --git a/app/src/main/java/eu/faircode/email/FragmentRule.java b/app/src/main/java/eu/faircode/email/FragmentRule.java index 6534ffe800..2c7322eaf8 100644 --- a/app/src/main/java/eu/faircode/email/FragmentRule.java +++ b/app/src/main/java/eu/faircode/email/FragmentRule.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Color; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; @@ -146,6 +147,9 @@ public class FragmentRule extends FragmentBase { private Button btnTtsSetup; private Button btnTtsData; + private Button btnSound; + private CheckBox cbAlarm; + private TextView tvAutomation; private BottomNavigationView bottom_navigation; @@ -160,6 +164,7 @@ public class FragmentRule extends FragmentBase { private Group grpMoveProp; private Group grpAnswer; private Group grpTts; + private Group grpSound; private Group grpAutomation; private Group grpDelete; @@ -174,6 +179,7 @@ public class FragmentRule extends FragmentBase { private long account = -1; private int protocol = -1; private long folder = -1; + private Uri sound = null; private DateFormat DF; @@ -188,8 +194,9 @@ public class FragmentRule extends FragmentBase { private static final int REQUEST_TO = 7; private final static int REQUEST_TTS_CHECK = 8; private final static int REQUEST_TTS_DATA = 9; - private final static int REQUEST_DATE_AFTER = 10; - private final static int REQUEST_DATE_BEFORE = 11; + private final static int REQUEST_SOUND = 10; + private final static int REQUEST_DATE_AFTER = 11; + private final static int REQUEST_DATE_BEFORE = 12; @Override public void onCreate(Bundle savedInstanceState) { @@ -205,6 +212,9 @@ public class FragmentRule extends FragmentBase { protocol = args.getInt("protocol", EntityAccount.TYPE_IMAP); folder = args.getLong("folder", -1); + if (savedInstanceState != null) + sound = savedInstanceState.getParcelable("fair:sound"); + DF = Helper.getDateTimeInstance(getContext(), DateFormat.SHORT, DateFormat.SHORT); } @@ -284,6 +294,10 @@ public class FragmentRule extends FragmentBase { btnTtsSetup = view.findViewById(R.id.btnTtsSetup); btnTtsData = view.findViewById(R.id.btnTtsData); + + btnSound = view.findViewById(R.id.btnSound); + cbAlarm = view.findViewById(R.id.cbAlarm); + tvAutomation = view.findViewById(R.id.tvAutomation); bottom_navigation = view.findViewById(R.id.bottom_navigation); @@ -299,6 +313,7 @@ public class FragmentRule extends FragmentBase { grpMoveProp = view.findViewById(R.id.grpMoveProp); grpAnswer = view.findViewById(R.id.grpAnswer); grpTts = view.findViewById(R.id.grpTts); + grpSound = view.findViewById(R.id.grpSound); grpAutomation = view.findViewById(R.id.grpAutomation); grpDelete = view.findViewById(R.id.grpDelete); @@ -461,6 +476,7 @@ public class FragmentRule extends FragmentBase { actions.add(new Action(EntityRule.TYPE_DELETE, getString(R.string.title_rule_delete))); 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_SOUND, getString(R.string.title_rule_sound))); actions.add(new Action(EntityRule.TYPE_AUTOMATION, getString(R.string.title_rule_automation))); adapterAction.addAll(actions); @@ -552,6 +568,17 @@ public class FragmentRule extends FragmentBase { } }); + btnSound.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALL); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getString(R.string.title_advanced_sound)); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, sound); + startActivityForResult(Helper.getChooser(getContext(), intent), REQUEST_SOUND); + } + }); + tvAutomation.setText(getString(R.string.title_rule_automation_hint, EntityRule.ACTION_AUTOMATION, TextUtils.join(",", new String[]{ @@ -589,6 +616,7 @@ public class FragmentRule extends FragmentBase { grpMoveProp.setVisibility(View.GONE); grpAnswer.setVisibility(View.GONE); grpTts.setVisibility(View.GONE); + grpSound.setVisibility(View.GONE); grpAutomation.setVisibility(View.GONE); grpDelete.setVisibility(View.GONE); @@ -695,6 +723,7 @@ public class FragmentRule extends FragmentBase { outState.putInt("fair:target", spTarget.getSelectedItemPosition()); outState.putInt("fair:identity", spIdent.getSelectedItemPosition()); outState.putInt("fair:answer", spAnswer.getSelectedItemPosition()); + outState.putParcelable("fair:sound", sound); super.onSaveInstanceState(outState); } @@ -751,6 +780,10 @@ public class FragmentRule extends FragmentBase { break; case REQUEST_TTS_DATA: break; + case REQUEST_SOUND: + if (resultCode == RESULT_OK && data != null) + onSelectSound(data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)); + break; case REQUEST_DATE_AFTER: if (resultCode == RESULT_OK && data != null) onDateAfter(data.getBundleExtra("args")); @@ -781,6 +814,10 @@ public class FragmentRule extends FragmentBase { } } + private void onSelectSound(Uri uri) { + this.sound = uri; + } + private void onDelete() { Bundle args = new Bundle(); args.putLong("id", id); @@ -1002,6 +1039,12 @@ public class FragmentRule extends FragmentBase { cbCc.setChecked(jaction.optBoolean("cc")); cbWithAttachments.setChecked(jaction.optBoolean("attachments")); break; + + case EntityRule.TYPE_SOUND: + if (jaction.has("uri")) + FragmentRule.this.sound = Uri.parse(jaction.getString("uri")); + cbAlarm.setChecked(jaction.optBoolean("alarm")); + break; } for (int pos = 0; pos < adapterAction.getCount(); pos++) @@ -1053,6 +1096,7 @@ public class FragmentRule extends FragmentBase { 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); + grpSound.setVisibility(type == EntityRule.TYPE_SOUND ? View.VISIBLE : View.GONE); grpAutomation.setVisibility(type == EntityRule.TYPE_AUTOMATION ? View.VISIBLE : View.GONE); grpDelete.setVisibility(type == EntityRule.TYPE_DELETE ? View.VISIBLE : View.GONE); } @@ -1329,6 +1373,11 @@ public class FragmentRule extends FragmentBase { jaction.put("cc", cbCc.isChecked()); jaction.put("attachments", cbWithAttachments.isChecked()); break; + + case EntityRule.TYPE_SOUND: + jaction.put("uri", sound); + jaction.put("alarm", cbAlarm.isChecked()); + break; } } diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index 41ee111ce2..b0dd2cb0b6 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -37,7 +37,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -58,6 +57,7 @@ import android.provider.Settings; import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.security.KeyChainException; +import android.telephony.TelephonyManager; import android.text.Html; import android.text.Layout; import android.text.Spannable; @@ -73,7 +73,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.EditorInfo; @@ -112,7 +111,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.bottomnavigation.BottomNavigationView; -import org.jsoup.helper.HttpConnection; import org.openintents.openpgp.util.OpenPgpApi; import java.io.ByteArrayOutputStream; diff --git a/app/src/main/java/eu/faircode/email/MediaPlayerHelper.java b/app/src/main/java/eu/faircode/email/MediaPlayerHelper.java new file mode 100644 index 0000000000..3f8663b2ed --- /dev/null +++ b/app/src/main/java/eu/faircode/email/MediaPlayerHelper.java @@ -0,0 +1,52 @@ +package eu.faircode.email; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.MediaPlayer; +import android.net.Uri; + +import java.io.IOException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class MediaPlayerHelper { + private static final int MAX_DURATION = 30; // seconds + + static void play(Context context, Uri uri, boolean alarm) throws IOException { + Semaphore sem = new Semaphore(0); + + AudioAttributes attrs = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(alarm ? AudioAttributes.USAGE_ALARM : AudioAttributes.USAGE_NOTIFICATION) + .build(); + + MediaPlayer mediaPlayer = new MediaPlayer(); + mediaPlayer.setAudioAttributes(attrs); + mediaPlayer.setDataSource(context.getApplicationContext(), uri); + mediaPlayer.setLooping(false); + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mp) { + mp.start(); + } + }); + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + mp.stop(); + mp.release(); + sem.release(); + } + }); + mediaPlayer.prepareAsync(); + + try { + if (!sem.tryAcquire(MAX_DURATION, TimeUnit.SECONDS)) { + mediaPlayer.stop(); + mediaPlayer.release(); + } + } catch (Throwable ex) { + Log.w(ex); + } + } +} diff --git a/app/src/main/res/layout/fragment_rule.xml b/app/src/main/res/layout/fragment_rule.xml index ce397c0833..134ff399d8 100644 --- a/app/src/main/res/layout/fragment_rule.xml +++ b/app/src/main/res/layout/fragment_rule.xml @@ -916,6 +916,37 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/btnTtsSetup" /> +