diff --git a/android/.idea/assetWizardSettings.xml b/android/.idea/assetWizardSettings.xml index 364e083..65911ec 100644 --- a/android/.idea/assetWizardSettings.xml +++ b/android/.idea/assetWizardSettings.xml @@ -14,8 +14,8 @@ diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/FI.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/FI.java new file mode 100644 index 0000000..e04ef5c --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/FI.java @@ -0,0 +1,29 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +import android.widget.SeekBar; + +public final class FI +{ + private FI() throws InstantiationException { throw new InstantiationException(); } + + public static SeekBar.OnSeekBarChangeListener SeekBarChanged(Func3to0 action) + { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + action.invoke(seekBar, progress, fromUser); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }; + } + +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func3to0.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func3to0.java new file mode 100644 index 0000000..f2bc4bb --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func3to0.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func3to0 { + void invoke(TInput1 value1, TInput2 value2, TInput3 value3); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func4to0.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func4to0.java new file mode 100644 index 0000000..2786c28 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func4to0.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func4to0 { + void invoke(TInput1 value1, TInput2 value2, TInput3 value3, TInput4 value4); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/NotificationSettings.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/NotificationSettings.java index 069fb97..ff37d7f 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/NotificationSettings.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/NotificationSettings.java @@ -11,6 +11,9 @@ public class NotificationSettings public String SoundSource; public boolean RepeatSound; + public boolean ForceVolume; + public int ForceVolumeValue; + public boolean EnableLED; public int LEDColor; @@ -18,12 +21,14 @@ public class NotificationSettings public NotificationSettings(PriorityEnum p) { - EnableSound = (p == PriorityEnum.HIGH); - SoundName = (p == PriorityEnum.HIGH) ? "Default" : ""; - SoundSource = (p == PriorityEnum.HIGH) ? RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toString() : Uri.EMPTY.toString(); - RepeatSound = false; - EnableLED = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL); - LEDColor = Color.BLUE; - EnableVibration = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL); + EnableSound = (p == PriorityEnum.HIGH); + SoundName = "Default"; + SoundSource = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toString(); + RepeatSound = false; + EnableLED = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL); + LEDColor = Color.BLUE; + EnableVibration = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL); + ForceVolume = false; + ForceVolumeValue = 50; } } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java index 0500d0a..a059397 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java @@ -71,29 +71,35 @@ public class SCNSettings Enabled = sharedPref.getBoolean("app_enabled", Enabled); LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize); - PriorityLow.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED); - PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound); - PriorityLow.EnableVibration = sharedPref.getBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration); - PriorityLow.RepeatSound = sharedPref.getBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound); - PriorityLow.SoundName = sharedPref.getString( "priority_low:sound_name", PriorityLow.SoundName); - PriorityLow.SoundSource = sharedPref.getString( "priority_low:sound_source", PriorityLow.SoundSource); - PriorityLow.LEDColor = sharedPref.getInt( "priority_low:led_color", PriorityLow.LEDColor); + PriorityLow.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED); + PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound); + PriorityLow.EnableVibration = sharedPref.getBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration); + PriorityLow.RepeatSound = sharedPref.getBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound); + PriorityLow.SoundName = sharedPref.getString( "priority_low:sound_name", PriorityLow.SoundName); + PriorityLow.SoundSource = sharedPref.getString( "priority_low:sound_source", PriorityLow.SoundSource); + PriorityLow.LEDColor = sharedPref.getInt( "priority_low:led_color", PriorityLow.LEDColor); + PriorityLow.ForceVolume = sharedPref.getBoolean("priority_low:force_volume", PriorityLow.ForceVolume); + PriorityLow.ForceVolumeValue = sharedPref.getInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue); - PriorityNorm.EnableLED = sharedPref.getBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED); - PriorityNorm.EnableSound = sharedPref.getBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound); - PriorityNorm.EnableVibration = sharedPref.getBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration); - PriorityNorm.RepeatSound = sharedPref.getBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound); - PriorityNorm.SoundName = sharedPref.getString( "priority_norm:sound_name", PriorityNorm.SoundName); - PriorityNorm.SoundSource = sharedPref.getString( "priority_norm:sound_source", PriorityNorm.SoundSource); - PriorityNorm.LEDColor = sharedPref.getInt( "priority_norm:led_color", PriorityNorm.LEDColor); + PriorityNorm.EnableLED = sharedPref.getBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED); + PriorityNorm.EnableSound = sharedPref.getBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound); + PriorityNorm.EnableVibration = sharedPref.getBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration); + PriorityNorm.RepeatSound = sharedPref.getBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound); + PriorityNorm.SoundName = sharedPref.getString( "priority_norm:sound_name", PriorityNorm.SoundName); + PriorityNorm.SoundSource = sharedPref.getString( "priority_norm:sound_source", PriorityNorm.SoundSource); + PriorityNorm.LEDColor = sharedPref.getInt( "priority_norm:led_color", PriorityNorm.LEDColor); + PriorityNorm.ForceVolume = sharedPref.getBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume); + PriorityNorm.ForceVolumeValue = sharedPref.getInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue); - PriorityHigh.EnableLED = sharedPref.getBoolean("priority_high:enabled_led", PriorityHigh.EnableLED); - PriorityHigh.EnableSound = sharedPref.getBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound); - PriorityHigh.EnableVibration = sharedPref.getBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration); - PriorityHigh.RepeatSound = sharedPref.getBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound); - PriorityHigh.SoundName = sharedPref.getString( "priority_high:sound_name", PriorityHigh.SoundName); - PriorityHigh.SoundSource = sharedPref.getString( "priority_high:sound_source", PriorityHigh.SoundSource); - PriorityHigh.LEDColor = sharedPref.getInt( "priority_high:led_color", PriorityHigh.LEDColor); + PriorityHigh.EnableLED = sharedPref.getBoolean("priority_high:enabled_led", PriorityHigh.EnableLED); + PriorityHigh.EnableSound = sharedPref.getBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound); + PriorityHigh.EnableVibration = sharedPref.getBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration); + PriorityHigh.RepeatSound = sharedPref.getBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound); + PriorityHigh.SoundName = sharedPref.getString( "priority_high:sound_name", PriorityHigh.SoundName); + PriorityHigh.SoundSource = sharedPref.getString( "priority_high:sound_source", PriorityHigh.SoundSource); + PriorityHigh.LEDColor = sharedPref.getInt( "priority_high:led_color", PriorityHigh.LEDColor); + PriorityHigh.ForceVolume = sharedPref.getBoolean("priority_high:force_volume", PriorityHigh.ForceVolume); + PriorityHigh.ForceVolumeValue = sharedPref.getInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue); } public void save() @@ -101,39 +107,45 @@ public class SCNSettings SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE); SharedPreferences.Editor e = sharedPref.edit(); - e.putInt( "quota_curr", quota_curr); - e.putInt( "quota_max", quota_max); - e.putInt( "user_id", user_id); - e.putString( "user_key", user_key); - e.putString( "fcm_token_local", fcm_token_local); - e.putString( "fcm_token_server", fcm_token_server); + e.putInt( "quota_curr", quota_curr); + e.putInt( "quota_max", quota_max); + e.putInt( "user_id", user_id); + e.putString( "user_key", user_key); + e.putString( "fcm_token_local", fcm_token_local); + e.putString( "fcm_token_server", fcm_token_server); - e.putBoolean("app_enabled", Enabled); - e.putInt( "local_cache_size", LocalCacheSize); + e.putBoolean("app_enabled", Enabled); + e.putInt( "local_cache_size", LocalCacheSize); - e.putBoolean("priority_low:enabled_led", PriorityLow.EnableLED); - e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound); - e.putBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration); - e.putBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound); - e.putString( "priority_low:sound_name", PriorityLow.SoundName); - e.putString( "priority_low:sound_source", PriorityLow.SoundSource); - e.putInt( "priority_low:led_color", PriorityLow.LEDColor); + e.putBoolean("priority_low:enabled_led", PriorityLow.EnableLED); + e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound); + e.putBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration); + e.putBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound); + e.putString( "priority_low:sound_name", PriorityLow.SoundName); + e.putString( "priority_low:sound_source", PriorityLow.SoundSource); + e.putInt( "priority_low:led_color", PriorityLow.LEDColor); + e.putBoolean("priority_low:force_volume", PriorityLow.ForceVolume); + e.putInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue); - e.putBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED); - e.putBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound); - e.putBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration); - e.putBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound); - e.putString( "priority_norm:sound_name", PriorityNorm.SoundName); - e.putString( "priority_norm:sound_source", PriorityNorm.SoundSource); - e.putInt( "priority_norm:led_color", PriorityNorm.LEDColor); + e.putBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED); + e.putBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound); + e.putBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration); + e.putBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound); + e.putString( "priority_norm:sound_name", PriorityNorm.SoundName); + e.putString( "priority_norm:sound_source", PriorityNorm.SoundSource); + e.putInt( "priority_norm:led_color", PriorityNorm.LEDColor); + e.putBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume); + e.putInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue); - e.putBoolean("priority_high:enabled_led", PriorityHigh.EnableLED); - e.putBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound); - e.putBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration); - e.putBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound); - e.putString( "priority_high:sound_name", PriorityHigh.SoundName); - e.putString( "priority_high:sound_source", PriorityHigh.SoundSource); - e.putInt( "priority_high:led_color", PriorityHigh.LEDColor); + e.putBoolean("priority_high:enabled_led", PriorityHigh.EnableLED); + e.putBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound); + e.putBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration); + e.putBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound); + e.putString( "priority_high:sound_name", PriorityHigh.SoundName); + e.putString( "priority_high:sound_source", PriorityHigh.SoundSource); + e.putInt( "priority_high:led_color", PriorityHigh.LEDColor); + e.putBoolean("priority_high:force_volume", PriorityHigh.ForceVolume); + e.putInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue); e.apply(); } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SoundService.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SoundService.java new file mode 100644 index 0000000..9fff131 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SoundService.java @@ -0,0 +1,43 @@ +package com.blackforestbytes.simplecloudnotifier.model; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; + +import com.blackforestbytes.simplecloudnotifier.SCNApp; +import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils; +import com.blackforestbytes.simplecloudnotifier.lib.string.Str; + +public class SoundService +{ + public static void playForegroundNoLooping(boolean enableSound, String soundSource, boolean forceVolume, int forceVolumeValue) + { + if (!enableSound) return; + if (Str.isNullOrWhitespace(soundSource)) return; + + if (forceVolume) + { + AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE); + int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + aman.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(maxVolume * (forceVolumeValue / 100.0)), 0); + + MediaPlayer player = MediaPlayer.create(SCNApp.getMainActivity(), Uri.parse(soundSource)); + player.setLooping(false); + player.setOnCompletionListener( mp -> { mp.stop(); mp.release(); }); + player.setOnSeekCompleteListener(mp -> { mp.stop(); mp.release(); }); + player.start(); + } + else + { + Ringtone rt = RingtoneManager.getRingtone(SCNApp.getContext(), Uri.parse(soundSource)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) rt.setLooping(false); + rt.play(); + + new Thread(() -> { ThreadUtils.safeSleep(5*1000); rt.stop(); }).start(); + } + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java index 48ee393..8fe28c8 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java @@ -6,10 +6,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.media.AudioAttributes; import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.VibrationEffect; @@ -18,12 +15,15 @@ import android.widget.Toast; import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.SCNApp; +import com.blackforestbytes.simplecloudnotifier.lib.string.Str; import com.blackforestbytes.simplecloudnotifier.model.CMessage; import com.blackforestbytes.simplecloudnotifier.model.NotificationSettings; import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum; import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; +import com.blackforestbytes.simplecloudnotifier.model.SoundService; import com.blackforestbytes.simplecloudnotifier.view.MainActivity; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; public class NotificationService @@ -43,10 +43,10 @@ public class NotificationService private NotificationService() { - updateChannels(); + createChannels(); } - public void updateChannels() + private void createChannels() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; @@ -79,13 +79,7 @@ public class NotificationService case HIGH: ns = SCNSettings.inst().PriorityHigh; break; } - if (ns.EnableSound && !ns.SoundSource.isEmpty()) - { - Ringtone rt = RingtoneManager.getRingtone(SCNApp.getContext(), Uri.parse(ns.SoundSource)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) rt.setLooping(false); - rt.play(); - new Thread(() -> { try { Thread.sleep(5*1000); } catch (InterruptedException e) { /* */ } rt.stop(); }).start(); - } + SoundService.playForegroundNoLooping(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue); if (ns.EnableVibration) { @@ -119,74 +113,100 @@ public class NotificationService { // old - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID); - mBuilder.setSmallIcon(R.drawable.ic_bfb); - mBuilder.setContentTitle(msg.Title); - mBuilder.setContentText(msg.Content); - mBuilder.setShowWhen(true); - mBuilder.setWhen(msg.Timestamp * 1000); - mBuilder.setAutoCancel(true); - - if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT); - if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); - if (ns.EnableVibration) mBuilder.setVibrate(new long[]{500}); - if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500); - - if (ns.EnableSound && !ns.SoundSource.isEmpty()) mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_NOTIFICATION); - - Intent intent = new Intent(ctxt, MainActivity.class); - PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0); - mBuilder.setContentIntent(pi); - NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); - - Notification n = mBuilder.build(); - if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound) n.flags |= Notification.FLAG_INSISTENT; - - if (mNotificationManager != null) mNotificationManager.notify(0, n); + showBackground_old(msg, ctxt, ns); } else { // new - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID); - mBuilder.setSmallIcon(R.drawable.ic_bfb); - mBuilder.setContentTitle(msg.Title); - mBuilder.setContentText(msg.Content); - mBuilder.setShowWhen(true); - mBuilder.setWhen(msg.Timestamp * 1000); - mBuilder.setAutoCancel(true); - - if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500); - - if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT); - if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); - - Intent intent = new Intent(ctxt, MainActivity.class); - PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0); - mBuilder.setContentIntent(pi); - NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); - if (mNotificationManager == null) return; - - Notification n = mBuilder.build(); - n.flags |= Notification.FLAG_AUTO_CANCEL; - - mNotificationManager.notify(0, n); - - if (ns.EnableSound && !ns.SoundSource.isEmpty()) - { - Ringtone rt = RingtoneManager.getRingtone(SCNApp.getContext(), Uri.parse(ns.SoundSource)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) rt.setLooping(false); - rt.play(); - new Thread(() -> { try { Thread.sleep(5*1000); } catch (InterruptedException e) { /* */ } rt.stop(); }).start(); - } - - if (ns.EnableVibration) - { - Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE); - v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE)); - } + showBackground_new(msg, ctxt, ns); } } + + private void showBackground_old(CMessage msg, Context ctxt, NotificationSettings ns) { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID); + mBuilder.setSmallIcon(R.drawable.ic_bfb); + mBuilder.setContentTitle(msg.Title); + mBuilder.setContentText(msg.Content); + mBuilder.setShowWhen(true); + mBuilder.setWhen(msg.Timestamp * 1000); + mBuilder.setAutoCancel(true); + + if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT); + if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); + if (ns.EnableVibration) mBuilder.setVibrate(new long[]{500}); + if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500); + + if (ns.EnableSound && !ns.SoundSource.isEmpty()) mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_NOTIFICATION); + + Intent intent = new Intent(ctxt, MainActivity.class); + PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0); + mBuilder.setContentIntent(pi); + NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); + + Notification n = mBuilder.build(); + if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound) n.flags |= Notification.FLAG_INSISTENT; + + if (mNotificationManager != null) mNotificationManager.notify(0, n); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void showBackground_new(CMessage msg, Context ctxt, NotificationSettings ns) { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID); + mBuilder.setSmallIcon(R.drawable.ic_bfb); + mBuilder.setContentTitle(msg.Title); + mBuilder.setContentText(msg.Content); + mBuilder.setShowWhen(true); + mBuilder.setWhen(msg.Timestamp * 1000); + mBuilder.setAutoCancel(true); + + if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500); + + if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT); + if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); + + if (ns.ForceVolume) + { + AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE); + int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); + aman.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(maxVolume * (ns.ForceVolumeValue / 100.0)), 0); + } + + Intent intent = new Intent(ctxt, MainActivity.class); + PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0); + mBuilder.setContentIntent(pi); + NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); + if (mNotificationManager == null) return; + + Notification n = mBuilder.build(); + n.flags |= Notification.FLAG_AUTO_CANCEL; + + mNotificationManager.notify(0, n); + + if (ns.EnableSound && !Str.isNullOrWhitespace(ns.SoundSource)) + { + if (ns.RepeatSound) + { + //TODO + } + else + { + SoundService.playForegroundNoLooping(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue); + } + } + + if (ns.EnableVibration) + { + Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE)); + } + + if (ns.EnableLED) + { + //TODO + } + } + } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java index ee6828d..9449b31 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java @@ -39,6 +39,22 @@ public class MainActivity extends AppCompatActivity tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); tabLayout.setupWithViewPager(viewPager); + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* */ } + + @Override + public void onPageSelected(int position) + { + if (position != 2) adpTabs.tab3.onViewpagerHide(); + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + SCNApp.register(this); IABService.startup(this); SCNSettings.inst().work(this); diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java index 6e3d5eb..c8c3401 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java @@ -1,8 +1,13 @@ package com.blackforestbytes.simplecloudnotifier.view; import android.content.Context; +import android.graphics.Color; import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.Ringtone; +import android.media.RingtoneManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -11,15 +16,19 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageView; +import android.widget.SeekBar; import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; import com.android.billingclient.api.Purchase; import com.blackforestbytes.simplecloudnotifier.R; +import com.blackforestbytes.simplecloudnotifier.SCNApp; +import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils; +import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI; +import com.blackforestbytes.simplecloudnotifier.lib.string.Str; import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; import com.blackforestbytes.simplecloudnotifier.service.IABService; -import com.blackforestbytes.simplecloudnotifier.service.NotificationService; import org.jetbrains.annotations.NotNull; @@ -45,6 +54,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener private View prefMsgLowLedColor_container; private ImageView prefMsgLowLedColor_value; private Switch prefMsgLowEnableVibrations; + private Switch prefMsgLowForceVolume; + private SeekBar prefMsgLowVolume; + private ImageView prefMsgLowVolumeTest; private Switch prefMsgNormEnableSound; private TextView prefMsgNormRingtone_value; @@ -54,6 +66,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener private View prefMsgNormLedColor_container; private ImageView prefMsgNormLedColor_value; private Switch prefMsgNormEnableVibrations; + private Switch prefMsgNormForceVolume; + private SeekBar prefMsgNormVolume; + private ImageView prefMsgNormVolumeTest; private Switch prefMsgHighEnableSound; private TextView prefMsgHighRingtone_value; @@ -63,9 +78,14 @@ public class SettingsFragment extends Fragment implements MusicPickerListener private View prefMsgHighLedColor_container; private ImageView prefMsgHighLedColor_value; private Switch prefMsgHighEnableVibrations; + private Switch prefMsgHighForceVolume; + private SeekBar prefMsgHighVolume; + private ImageView prefMsgHighVolumeTest; private int musicPickerSwitch = -1; + private MediaPlayer[] mPlayers = new MediaPlayer[3]; + public SettingsFragment() { // Required empty public constructor @@ -99,6 +119,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgLowLedColor_value = v.findViewById(R.id.prefMsgLowLedColor_value); prefMsgLowLedColor_container = v.findViewById(R.id.prefMsgLowLedColor_container); prefMsgLowEnableVibrations = v.findViewById(R.id.prefMsgLowEnableVibrations); + prefMsgLowForceVolume = v.findViewById(R.id.prefMsgLowForceVolume); + prefMsgLowVolume = v.findViewById(R.id.prefMsgLowVolume); + prefMsgLowVolumeTest = v.findViewById(R.id.btnLowVolumeTest); prefMsgNormEnableSound = v.findViewById(R.id.prefMsgNormEnableSound); prefMsgNormRingtone_value = v.findViewById(R.id.prefMsgNormRingtone_value); @@ -108,6 +131,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgNormLedColor_value = v.findViewById(R.id.prefMsgNormLedColor_value); prefMsgNormLedColor_container = v.findViewById(R.id.prefMsgNormLedColor_container); prefMsgNormEnableVibrations = v.findViewById(R.id.prefMsgNormEnableVibrations); + prefMsgNormForceVolume = v.findViewById(R.id.prefMsgNormForceVolume); + prefMsgNormVolume = v.findViewById(R.id.prefMsgNormVolume); + prefMsgNormVolumeTest = v.findViewById(R.id.btnNormVolumeTest); prefMsgHighEnableSound = v.findViewById(R.id.prefMsgHighEnableSound); prefMsgHighRingtone_value = v.findViewById(R.id.prefMsgHighRingtone_value); @@ -117,6 +143,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgHighLedColor_value = v.findViewById(R.id.prefMsgHighLedColor_value); prefMsgHighLedColor_container = v.findViewById(R.id.prefMsgHighLedColor_container); prefMsgHighEnableVibrations = v.findViewById(R.id.prefMsgHighEnableVibrations); + prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume); + prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume); + prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest); } private void updateUI() @@ -142,6 +171,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener if (prefMsgLowEnableLED.isChecked() != s.PriorityLow.EnableLED) prefMsgLowEnableLED.setChecked(s.PriorityLow.EnableLED); prefMsgLowLedColor_value.setColorFilter(s.PriorityLow.LEDColor); if (prefMsgLowEnableVibrations.isChecked() != s.PriorityLow.EnableVibration) prefMsgLowEnableVibrations.setChecked(s.PriorityLow.EnableVibration); + if (prefMsgLowForceVolume.isChecked() != s.PriorityLow.ForceVolume) prefMsgLowForceVolume.setChecked(s.PriorityLow.ForceVolume); + if (prefMsgLowVolume.getMax() != 100) prefMsgLowVolume.setMax(100); + if (prefMsgLowVolume.getProgress() != s.PriorityLow.ForceVolumeValue) prefMsgLowVolume.setProgress(s.PriorityLow.ForceVolumeValue); + if (prefMsgLowVolume.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolume.setEnabled(s.PriorityLow.ForceVolume); + if (prefMsgLowVolumeTest.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setEnabled(s.PriorityLow.ForceVolume); + if (s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setColorFilter(null); else prefMsgLowVolumeTest.setColorFilter(Color.argb(150,200,200,200)); if (prefMsgNormEnableSound.isChecked() != s.PriorityNorm.EnableSound) prefMsgNormEnableSound.setChecked(s.PriorityNorm.EnableSound); if (!prefMsgNormRingtone_value.getText().equals(s.PriorityNorm.SoundName)) prefMsgNormRingtone_value.setText(s.PriorityNorm.SoundName); @@ -149,6 +184,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener if (prefMsgNormEnableLED.isChecked() != s.PriorityNorm.EnableLED) prefMsgNormEnableLED.setChecked(s.PriorityNorm.EnableLED); prefMsgNormLedColor_value.setColorFilter(s.PriorityNorm.LEDColor); if (prefMsgNormEnableVibrations.isChecked() != s.PriorityNorm.EnableVibration) prefMsgNormEnableVibrations.setChecked(s.PriorityNorm.EnableVibration); + if (prefMsgNormForceVolume.isChecked() != s.PriorityNorm.ForceVolume) prefMsgNormForceVolume.setChecked(s.PriorityNorm.ForceVolume); + if (prefMsgNormVolume.getMax() != 100) prefMsgNormVolume.setMax(100); + if (prefMsgNormVolume.getProgress() != s.PriorityNorm.ForceVolumeValue) prefMsgNormVolume.setProgress(s.PriorityNorm.ForceVolumeValue); + if (prefMsgNormVolume.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolume.setEnabled(s.PriorityNorm.ForceVolume); + if (prefMsgNormVolumeTest.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setEnabled(s.PriorityNorm.ForceVolume); + if (s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setColorFilter(null); else prefMsgNormVolumeTest.setColorFilter(Color.argb(150,200,200,200)); if (prefMsgHighEnableSound.isChecked() != s.PriorityHigh.EnableSound) prefMsgHighEnableSound.setChecked(s.PriorityHigh.EnableSound); if (!prefMsgHighRingtone_value.getText().equals(s.PriorityHigh.SoundName)) prefMsgHighRingtone_value.setText(s.PriorityHigh.SoundName); @@ -156,6 +197,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener if (prefMsgHighEnableLED.isChecked() != s.PriorityHigh.EnableLED) prefMsgHighEnableLED.setChecked(s.PriorityHigh.EnableLED); prefMsgHighLedColor_value.setColorFilter(s.PriorityHigh.LEDColor); if (prefMsgHighEnableVibrations.isChecked() != s.PriorityHigh.EnableVibration) prefMsgHighEnableVibrations.setChecked(s.PriorityHigh.EnableVibration); + if (prefMsgHighForceVolume.isChecked() != s.PriorityHigh.ForceVolume) prefMsgHighForceVolume.setChecked(s.PriorityHigh.ForceVolume); + if (prefMsgHighVolume.getMax() != 100) prefMsgHighVolume.setMax(100); + if (prefMsgHighVolume.getProgress() != s.PriorityHigh.ForceVolumeValue) prefMsgHighVolume.setProgress(s.PriorityHigh.ForceVolumeValue); + if (prefMsgHighVolume.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolume.setEnabled(s.PriorityHigh.ForceVolume); + if (prefMsgHighVolumeTest.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setEnabled(s.PriorityHigh.ForceVolume); + if (s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setColorFilter(null); else prefMsgHighVolumeTest.setColorFilter(Color.argb(150,200,200,200)); } private void initListener() @@ -181,6 +228,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgLowEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableLED=b; saveAndUpdate(); }); prefMsgLowLedColor_container.setOnClickListener(a -> chooseLEDColorLow()); prefMsgLowEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableVibration=b; saveAndUpdate(); }); + prefMsgLowForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.ForceVolume=b; saveAndUpdate(); }); + prefMsgLowVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityLow.ForceVolumeValue=b; saveAndUpdate(); updateVolume(0, b); } })); + prefMsgLowVolumeTest.setOnClickListener((v) -> { if (s.PriorityLow.ForceVolume) playTestSound(0, prefMsgLowVolumeTest, s.PriorityLow.SoundSource, s.PriorityLow.ForceVolumeValue); }); prefMsgNormEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableSound=b; saveAndUpdate(); }); prefMsgNormRingtone_container.setOnClickListener(a -> chooseRingtoneNorm()); @@ -188,6 +238,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgNormEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableLED=b; saveAndUpdate(); }); prefMsgNormLedColor_container.setOnClickListener(a -> chooseLEDColorNorm()); prefMsgNormEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableVibration=b; saveAndUpdate(); }); + prefMsgNormForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.ForceVolume=b; saveAndUpdate(); }); + prefMsgNormVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityNorm.ForceVolumeValue=b; saveAndUpdate(); updateVolume(1, b); } })); + prefMsgNormVolumeTest.setOnClickListener((v) -> { if (s.PriorityNorm.ForceVolume) playTestSound(1, prefMsgNormVolumeTest, s.PriorityNorm.SoundSource, s.PriorityNorm.ForceVolumeValue); }); prefMsgHighEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableSound=b; saveAndUpdate(); }); prefMsgHighRingtone_container.setOnClickListener(a -> chooseRingtoneHigh()); @@ -195,6 +248,57 @@ public class SettingsFragment extends Fragment implements MusicPickerListener prefMsgHighEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableLED=b; saveAndUpdate(); }); prefMsgHighLedColor_container.setOnClickListener(a -> chooseLEDColorHigh()); prefMsgHighEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableVibration=b; saveAndUpdate(); }); + prefMsgHighForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.ForceVolume=b; saveAndUpdate(); }); + prefMsgHighVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityHigh.ForceVolumeValue=b; saveAndUpdate(); updateVolume(2, b); } })); + prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); }); + } + + private void updateVolume(int idx, int volume) + { + if (mPlayers[idx] != null && mPlayers[idx].isPlaying()) + { + AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE); + int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + aman.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(maxVolume * (volume / 100.0)), 0); + } + } + + private void stopSound(final int idx, final ImageView iv) + { + if (mPlayers[idx] != null && mPlayers[idx].isPlaying()) + { + mPlayers[idx].stop(); + mPlayers[idx].release(); + iv.setImageResource(R.drawable.ic_play); + mPlayers[idx] = null; + } + } + + private void playTestSound(final int idx, final ImageView iv, String src, int volume) + { + if (mPlayers[idx] != null && mPlayers[idx].isPlaying()) + { + mPlayers[idx].stop(); + mPlayers[idx].release(); + iv.setImageResource(R.drawable.ic_play); + mPlayers[idx] = null; + return; + } + + if (Str.isNullOrWhitespace(src)) return; + if (volume == 0) return; + + iv.setImageResource(R.drawable.ic_pause); + + AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE); + int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + aman.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(maxVolume * (volume / 100.0)), 0); + + MediaPlayer player = mPlayers[idx] = MediaPlayer.create(getActivity(), Uri.parse(src)); + player.setLooping(false); + player.setOnCompletionListener( mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); })); + player.setOnSeekCompleteListener(mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); })); + player.start(); } private void saveAndUpdate() @@ -355,4 +459,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener { musicPickerSwitch = -1; } + + public void onViewpagerHide() + { + stopSound(0, prefMsgLowVolumeTest); + stopSound(1, prefMsgNormVolumeTest); + stopSound(2, prefMsgHighVolumeTest); + } } \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_pause.xml b/android/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..d6e6c76 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_play.xml b/android/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..283f1e5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_volume.xml b/android/app/src/main/res/drawable/ic_volume.xml new file mode 100644 index 0000000..9f5bc68 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_volume.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/layout/fragment_settings.xml b/android/app/src/main/res/layout/fragment_settings.xml index adbca25..28fdecb 100644 --- a/android/app/src/main/res/layout/fragment_settings.xml +++ b/android/app/src/main/res/layout/fragment_settings.xml @@ -202,6 +202,60 @@ android:layout_marginEnd="4dp" android:minHeight="48dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enable notification sound Notification sound Repeat notification sound + Automatically set the volume Enable notification light Notification light color Enable notification vibration Upgrade account Thank you for supporting the app and using the pro mode Increase your daily quota, remove the ad banner and support the developer (that\'s me) + Volume icon + Play test sound