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" />
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/tvAlarm" />
+
+
Text to speech
Automation
Delete permanently
+ Play sound
Edit rule
Rule applies to
@@ -1543,6 +1544,10 @@
Subject
Text
+ Select sound
+ Use as alarm
+ This will ignore "do not disturb" rules
+
Synchronize
Folders
Messages
diff --git a/metadata/en-US/changelogs/1802.txt b/metadata/en-US/changelogs/1802.txt
index f0bd88844f..c1d0876d47 100644
--- a/metadata/en-US/changelogs/1802.txt
+++ b/metadata/en-US/changelogs/1802.txt
@@ -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